Update copyright in all files
[geeqie.git] / src / editors.c
1 /*
2  * Copyright (C) 2006 John Ellis
3  * Copyright (C) 2008 - 2016 The Geeqie Team
4  *
5  * Author: John Ellis
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 #include "main.h"
23 #include "editors.h"
24
25 #include "filedata.h"
26 #include "filefilter.h"
27 #include "misc.h"
28 #include "pixbuf_util.h"
29 #include "ui_fileops.h"
30 #include "ui_spinner.h"
31 #include "ui_utildlg.h"
32 #include "utilops.h"
33
34 #include <errno.h>
35
36
37 #define EDITOR_WINDOW_WIDTH 500
38 #define EDITOR_WINDOW_HEIGHT 300
39
40
41
42 typedef struct _EditorVerboseData EditorVerboseData;
43 struct _EditorVerboseData {
44         GenericDialog *gd;
45         GtkWidget *button_close;
46         GtkWidget *button_stop;
47         GtkWidget *text;
48         GtkWidget *progress;
49         GtkWidget *spinner;
50 };
51
52 typedef struct _EditorData EditorData;
53 struct _EditorData {
54         EditorFlags flags;
55         GPid pid;
56         GList *list;
57         gint count;
58         gint total;
59         gboolean stopping;
60         EditorVerboseData *vd;
61         EditorCallback callback;
62         gpointer data;
63         const EditorDescription *editor;
64         gchar *working_directory; /* fallback if no files are given (editor_no_param) */
65 };
66
67
68 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
69 static EditorFlags editor_command_next_start(EditorData *ed);
70 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
71 static EditorFlags editor_command_done(EditorData *ed);
72
73 /*
74  *-----------------------------------------------------------------------------
75  * external editor routines
76  *-----------------------------------------------------------------------------
77  */
78
79 GHashTable *editors = NULL;
80 GtkListStore *desktop_file_list;
81 gboolean editors_finished = FALSE;
82
83 #ifdef G_KEY_FILE_DESKTOP_GROUP
84 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
85 #else
86 #define DESKTOP_GROUP "Desktop Entry"
87 #endif
88
89 void editor_description_free(EditorDescription *editor)
90 {
91         if (!editor) return;
92
93         g_free(editor->key);
94         g_free(editor->name);
95         g_free(editor->icon);
96         g_free(editor->exec);
97         g_free(editor->menu_path);
98         g_free(editor->hotkey);
99         g_free(editor->comment);
100         string_list_free(editor->ext_list);
101         g_free(editor->file);
102         g_free(editor);
103 }
104
105 static GList *editor_mime_types_to_extensions(gchar **mime_types)
106 {
107         /* FIXME: this should be rewritten to use the shared mime database, as soon as we switch to gio */
108
109         static const gchar *conv_table[][2] = {
110                 {"application/x-ufraw", ".ufraw"},
111                 {"image/*",             "*"},
112                 {"image/bmp",           ".bmp"},
113                 {"image/gif",           ".gif"},
114                 {"image/jpeg",          ".jpeg;.jpg"},
115                 {"image/jpg",           ".jpg;.jpeg"},
116                 {"image/pcx",           ".pcx"},
117                 {"image/png",           ".png"},
118                 {"image/svg",           ".svg"},
119                 {"image/svg+xml",       ".svg"},
120                 {"image/svg+xml-compressed",    ".svg"},
121                 {"image/tiff",          ".tiff;.tif"},
122                 {"image/x-bmp",         ".bmp"},
123                 {"image/x-canon-crw",   ".crw"},
124                 {"image/x-cr2",         ".cr2"},
125                 {"image/x-dcraw",       "%raw"},
126                 {"image/x-ico",         ".ico"},
127                 {"image/x-mrw",         ".mrw"},
128                 {"image/x-MS-bmp",      ".bmp"},
129                 {"image/x-nef",         ".nef"},
130                 {"image/x-orf",         ".orf"},
131                 {"image/x-pcx",         ".pcx"},
132                 {"image/xpm",           ".xpm"},
133                 {"image/x-png",         ".png"},
134                 {"image/x-portable-anymap",     ".pam"},
135                 {"image/x-portable-bitmap",     ".pbm"},
136                 {"image/x-portable-graymap",    ".pgm"},
137                 {"image/x-portable-pixmap",     ".ppm"},
138                 {"image/x-psd",         ".psd"},
139                 {"image/x-raf",         ".raf"},
140                 {"image/x-sgi",         ".sgi"},
141                 {"image/x-tga",         ".tga"},
142                 {"image/x-xbitmap",     ".xbm"},
143                 {"image/x-xcf",         ".xcf"},
144                 {"image/x-xpixmap",     ".xpm"},
145                 {"image/x-x3f",         ".x3f"},
146                 {"application/x-ptoptimizer-script",    ".pto"},
147                 {NULL, NULL}};
148
149         gint i, j;
150         GList *list = NULL;
151
152         for (i = 0; mime_types[i]; i++)
153                 for (j = 0; conv_table[j][0]; j++)
154                         if (strcmp(mime_types[i], conv_table[j][0]) == 0)
155                                 list = g_list_concat(list, filter_to_list(conv_table[j][1]));
156
157         return list;
158 }
159
160 gboolean editor_read_desktop_file(const gchar *path)
161 {
162         GKeyFile *key_file;
163         EditorDescription *editor;
164         gchar *extensions;
165         gchar *type;
166         const gchar *key = filename_from_path(path);
167         gchar **categories, **only_show_in, **not_show_in;
168         gchar *try_exec;
169         GtkTreeIter iter;
170         gboolean category_geeqie = FALSE;
171
172         if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
173
174         key_file = g_key_file_new();
175         if (!g_key_file_load_from_file(key_file, path, 0, NULL))
176                 {
177                 g_key_file_free(key_file);
178                 return FALSE;
179                 }
180
181         type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
182         if (!type || strcmp(type, "Application") != 0)
183                 {
184                 /* We only consider desktop entries of Application type */
185                 g_key_file_free(key_file);
186                 g_free(type);
187                 return FALSE;
188                 }
189         g_free(type);
190
191         editor = g_new0(EditorDescription, 1);
192
193         editor->key = g_strdup(key);
194         editor->file = g_strdup(path);
195
196         g_hash_table_insert(editors, editor->key, editor);
197
198         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
199             || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
200                 {
201                 editor->hidden = TRUE;
202                 }
203
204         categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
205         if (categories)
206                 {
207                 gboolean found = FALSE;
208                 gint i;
209                 for (i = 0; categories[i]; i++)
210                         {
211                         /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
212                         if (strcmp(categories[i], "Graphics") == 0)
213                                 {
214                                 found = TRUE;
215                                 }
216                         if (strcmp(categories[i], "X-Geeqie") == 0)
217                                 {
218                                 found = TRUE;
219                                 category_geeqie = TRUE;
220                                 break;
221                                 }
222                         }
223                 if (!found) editor->ignored = TRUE;
224                 g_strfreev(categories);
225                 }
226         else
227                 {
228                 editor->ignored = TRUE;
229                 }
230
231         only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
232         if (only_show_in)
233                 {
234                 gboolean found = FALSE;
235                 gint i;
236                 for (i = 0; only_show_in[i]; i++)
237                         if (strcmp(only_show_in[i], "X-Geeqie") == 0)
238                                 {
239                                 found = TRUE;
240                                 break;
241                                 }
242                 if (!found) editor->ignored = TRUE;
243                 g_strfreev(only_show_in);
244                 }
245
246         not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
247         if (not_show_in)
248                 {
249                 gboolean found = FALSE;
250                 gint i;
251                 for (i = 0; not_show_in[i]; i++)
252                         if (strcmp(not_show_in[i], "X-Geeqie") == 0)
253                                 {
254                                 found = TRUE;
255                                 break;
256                                 }
257                 if (found) editor->ignored = TRUE;
258                 g_strfreev(not_show_in);
259                 }
260
261
262         try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
263         if (try_exec && !editor->hidden && !editor->ignored)
264                 {
265                 gchar *try_exec_res = g_find_program_in_path(try_exec);
266                 if (!try_exec_res) editor->hidden = TRUE;
267                 g_free(try_exec_res);
268                 g_free(try_exec);
269                 }
270
271         if (editor->ignored)
272                 {
273                 /* ignored editors will be deleted, no need to parse the rest */
274                 g_key_file_free(key_file);
275                 return TRUE;
276                 }
277
278         editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
279         editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
280
281         /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
282         if (editor->icon && !g_path_is_absolute(editor->icon))
283                 {
284                 gchar *ext = strrchr(editor->icon, '.');
285
286                 if (ext && strlen(ext) == 4 &&
287                     (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
288                         {
289                         log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
290                                    editor->file, editor->icon);
291
292                         // drop extension
293                         *ext = '\0';
294                         }
295                 }
296         if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
297                 {
298                 g_free(editor->icon);
299                 editor->icon = NULL;
300                 }
301
302         editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
303
304         editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
305         if (!editor->menu_path) editor->menu_path = g_strdup("EditMenu/ExternalMenu");
306
307         editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);
308
309         editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);
310
311         extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
312         if (extensions)
313                 editor->ext_list = filter_to_list(extensions);
314         else
315                 {
316                 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
317                 if (mime_types)
318                         {
319                         editor->ext_list = editor_mime_types_to_extensions(mime_types);
320                         g_strfreev(mime_types);
321                         if (!editor->ext_list) editor->hidden = TRUE;
322                         }
323                 }
324
325         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags |= EDITOR_KEEP_FS;
326         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags |= EDITOR_VERBOSE;
327         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags |= EDITOR_VERBOSE_MULTI;
328         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags |= EDITOR_DEST;
329         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags |= EDITOR_TERMINAL;
330
331         editor->flags |= editor_command_parse(editor, NULL, FALSE, NULL);
332
333         if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
334
335         g_key_file_free(key_file);
336
337         if (editor->ignored) return TRUE;
338
339         gtk_list_store_append(desktop_file_list, &iter);
340         gtk_list_store_set(desktop_file_list, &iter,
341                            DESKTOP_FILE_COLUMN_KEY, key,
342                            DESKTOP_FILE_COLUMN_NAME, editor->name,
343                            DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
344                            DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
345                            DESKTOP_FILE_COLUMN_PATH, path, -1);
346
347         return TRUE;
348 }
349
350 static gboolean editor_remove_desktop_file_cb(gpointer key, gpointer value, gpointer user_data)
351 {
352         EditorDescription *editor = value;
353         return editor->hidden || editor->ignored;
354 }
355
356 void editor_table_finish(void)
357 {
358         g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
359         editors_finished = TRUE;
360 }
361
362 void editor_table_clear(void)
363 {
364         if (desktop_file_list)
365                 {
366                 gtk_list_store_clear(desktop_file_list);
367                 }
368         else
369                 {
370                 desktop_file_list = gtk_list_store_new(DESKTOP_FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
371                 }
372         if (editors)
373                 {
374                 g_hash_table_destroy(editors);
375                 }
376         editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
377         editors_finished = FALSE;
378 }
379
380 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
381 {
382         DIR *dp;
383         struct dirent *dir;
384         gchar *pathl;
385
386         pathl = path_from_utf8(path);
387         dp = opendir(pathl);
388         g_free(pathl);
389         if (!dp)
390                 {
391                 /* dir not found */
392                 return list;
393                 }
394         while ((dir = readdir(dp)) != NULL)
395                 {
396                 gchar *namel = dir->d_name;
397
398                 if (g_str_has_suffix(namel, ".desktop"))
399                         {
400                         gchar *name = path_to_utf8(namel);
401                         gchar *dpath = g_build_filename(path, name, NULL);
402                         list = g_list_prepend(list, dpath);
403                         g_free(name);
404                         }
405                 }
406         closedir(dp);
407         return list;
408 }
409
410 GList *editor_get_desktop_files(void)
411 {
412         gchar *path;
413         gchar *xdg_data_dirs;
414         gchar *all_dirs;
415         gchar **split_dirs;
416         gint i;
417         GList *list = NULL;
418
419         xdg_data_dirs = getenv("XDG_DATA_DIRS");
420         if (xdg_data_dirs && xdg_data_dirs[0])
421                 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
422         else
423                 xdg_data_dirs = g_strdup("/usr/share");
424
425         all_dirs = g_strconcat(get_rc_dir(), ":", GQ_APP_DIR, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
426
427         g_free(xdg_data_dirs);
428
429         split_dirs = g_strsplit(all_dirs, ":", 0);
430
431         g_free(all_dirs);
432
433         for (i = 0; split_dirs[i]; i++);
434         for (--i; i >= 0; i--)
435                 {
436                 path = g_build_filename(split_dirs[i], "applications", NULL);
437                 list = editor_add_desktop_dir(list, path);
438                 g_free(path);
439                 }
440
441         g_strfreev(split_dirs);
442         return list;
443 }
444
445 static void editor_list_add_cb(gpointer key, gpointer value, gpointer data)
446 {
447         GList **listp = data;
448         EditorDescription *editor = value;
449
450         /* do not show the special commands in any list, they are called explicitly */
451         if (strcmp(editor->key, CMD_COPY) == 0 ||
452             strcmp(editor->key, CMD_MOVE) == 0 ||
453             strcmp(editor->key, CMD_RENAME) == 0 ||
454             strcmp(editor->key, CMD_DELETE) == 0 ||
455             strcmp(editor->key, CMD_FOLDER) == 0) return;
456
457         *listp = g_list_prepend(*listp, editor);
458 }
459
460 static gint editor_sort(gconstpointer a, gconstpointer b)
461 {
462         const EditorDescription *ea = a;
463         const EditorDescription *eb = b;
464         gint ret;
465
466         ret = strcmp(ea->menu_path, eb->menu_path);
467         if (ret != 0) return ret;
468
469         return g_utf8_collate(ea->name, eb->name);
470 }
471
472 GList *editor_list_get(void)
473 {
474         GList *editors_list = NULL;
475
476         if (!editors_finished) return NULL;
477
478         g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
479         editors_list = g_list_sort(editors_list, editor_sort);
480
481         return editors_list;
482 }
483
484 /* ------------------------------ */
485
486
487 static void editor_verbose_data_free(EditorData *ed)
488 {
489         if (!ed->vd) return;
490         g_free(ed->vd);
491         ed->vd = NULL;
492 }
493
494 static void editor_data_free(EditorData *ed)
495 {
496         editor_verbose_data_free(ed);
497         g_free(ed->working_directory);
498         g_free(ed);
499 }
500
501 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
502 {
503         EditorData *ed = data;
504
505         generic_dialog_close(gd);
506         editor_verbose_data_free(ed);
507         if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
508 }
509
510 static void editor_verbose_window_stop(GenericDialog *gd, gpointer data)
511 {
512         EditorData *ed = data;
513         ed->stopping = TRUE;
514         ed->count = 0;
515         editor_verbose_window_progress(ed, _("stopping..."));
516 }
517
518 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
519 {
520         vd->gd->cancel_cb = editor_verbose_window_close;
521
522         spinner_set_interval(vd->spinner, -1);
523         gtk_widget_set_sensitive(vd->button_stop, FALSE);
524         gtk_widget_set_sensitive(vd->button_close, TRUE);
525 }
526
527 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
528 {
529         EditorVerboseData *vd;
530         GtkWidget *scrolled;
531         GtkWidget *hbox;
532         gchar *buf;
533
534         vd = g_new0(EditorVerboseData, 1);
535
536         vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
537                                    NULL, FALSE,
538                                    NULL, ed);
539         buf = g_strdup_printf(_("Output of %s"), text);
540         generic_dialog_add_message(vd->gd, NULL, buf, NULL);
541         g_free(buf);
542         vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
543                                                    editor_verbose_window_stop, FALSE);
544         gtk_widget_set_sensitive(vd->button_stop, FALSE);
545         vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
546                                                     editor_verbose_window_close, TRUE);
547         gtk_widget_set_sensitive(vd->button_close, FALSE);
548
549         scrolled = gtk_scrolled_window_new(NULL, NULL);
550         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
551         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
552                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
553         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
554         gtk_widget_show(scrolled);
555
556         vd->text = gtk_text_view_new();
557         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
558         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
559         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
560         gtk_widget_show(vd->text);
561
562         hbox = gtk_hbox_new(FALSE, 0);
563         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
564         gtk_widget_show(hbox);
565
566         vd->progress = gtk_progress_bar_new();
567         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
568         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
569         gtk_widget_show(vd->progress);
570
571         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
572         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
573         gtk_widget_show(vd->spinner);
574
575         gtk_widget_show(vd->gd->dialog);
576
577         ed->vd = vd;
578         return vd;
579 }
580
581 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
582 {
583         GtkTextBuffer *buffer;
584         GtkTextIter iter;
585
586         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
587         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
588         gtk_text_buffer_insert(buffer, &iter, text, len);
589 }
590
591 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
592 {
593         if (!ed->vd) return;
594
595         if (ed->total)
596                 {
597                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
598                 }
599
600         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
601 }
602
603 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
604 {
605         EditorData *ed = data;
606         gchar buf[512];
607         gsize count;
608
609         if (condition & G_IO_IN)
610                 {
611                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
612                         {
613                         if (!g_utf8_validate(buf, count, NULL))
614                                 {
615                                 gchar *utf8;
616
617                                 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
618                                 if (utf8)
619                                         {
620                                         editor_verbose_window_fill(ed->vd, utf8, -1);
621                                         g_free(utf8);
622                                         }
623                                 else
624                                         {
625                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
626                                         }
627                                 }
628                         else
629                                 {
630                                 editor_verbose_window_fill(ed->vd, buf, count);
631                                 }
632                         }
633                 }
634
635         if (condition & (G_IO_ERR | G_IO_HUP))
636                 {
637                 g_io_channel_shutdown(source, TRUE, NULL);
638                 return FALSE;
639                 }
640
641         return TRUE;
642 }
643
644 typedef enum {
645         PATH_FILE,
646         PATH_FILE_URL,
647         PATH_DEST
648 } PathType;
649
650
651 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
652 {
653         GString *string;
654         gchar *pathl;
655         const gchar *p = NULL;
656
657         DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
658
659         string = g_string_new("");
660
661         if (type == PATH_FILE || type == PATH_FILE_URL)
662                 {
663                 GList *work = editor->ext_list;
664
665                 if (!work)
666                         p = fd->path;
667                 else
668                         {
669                         while (work)
670                                 {
671                                 GList *work2;
672                                 gchar *ext = work->data;
673                                 work = work->next;
674
675                                 if (strcmp(ext, "*") == 0 ||
676                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
677                                         {
678                                         p = fd->path;
679                                         break;
680                                         }
681
682                                 work2 = consider_sidecars ? fd->sidecar_files : NULL;
683                                 while (work2)
684                                         {
685                                         FileData *sfd = work2->data;
686                                         work2 = work2->next;
687
688                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
689                                                 {
690                                                 p = sfd->path;
691                                                 break;
692                                                 }
693                                         }
694                                 if (p) break;
695                                 }
696                         if (!p) return NULL;
697                         }
698                 }
699         else if (type == PATH_DEST)
700                 {
701                 if (fd->change && fd->change->dest)
702                         p = fd->change->dest;
703                 else
704                         p = "";
705                 }
706
707         g_assert(p);
708         string = g_string_append(string, p);
709
710         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
711         pathl = path_from_utf8(string->str);
712         g_string_free(string, TRUE);
713
714         if (pathl && !pathl[0]) /* empty string case */
715                 {
716                 g_free(pathl);
717                 pathl = NULL;
718                 }
719
720         DEBUG_2("editor_command_path_parse: return %s", pathl);
721         return pathl;
722 }
723
724 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
725 {
726         const char *p;
727
728         if (!single_quotes)
729                 {
730                 if (!double_quotes)
731                         g_string_append_c(str, '\'');
732                 else
733                         g_string_append(str, "\"'");
734                 }
735
736         for (p = s; *p != '\0'; p++)
737                 {
738                 if (*p == '\'')
739                         g_string_append(str, "'\\''");
740                 else
741                         g_string_append_c(str, *p);
742                 }
743
744         if (!single_quotes)
745                 {
746                 if (!double_quotes)
747                         g_string_append_c(str, '\'');
748                 else
749                         g_string_append(str, "'\"");
750                 }
751
752         return str;
753 }
754
755
756 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
757 {
758         EditorFlags flags = 0;
759         const gchar *p;
760         GString *result = NULL;
761         gboolean escape = FALSE;
762         gboolean single_quotes = FALSE;
763         gboolean double_quotes = FALSE;
764
765         DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
766
767         if (output)
768                 result = g_string_new("");
769
770         if (editor->exec == NULL || editor->exec[0] == '\0')
771                 {
772                 flags |= EDITOR_ERROR_EMPTY;
773                 goto err;
774                 }
775
776         p = editor->exec;
777         /* skip leading whitespaces if any */
778         while (g_ascii_isspace(*p)) p++;
779
780         /* command */
781
782         while (*p)
783                 {
784                 if (escape)
785                         {
786                         escape = FALSE;
787                         if (output) result = g_string_append_c(result, *p);
788                         }
789                 else if (*p == '\\')
790                         {
791                         if (!single_quotes) escape = TRUE;
792                         if (output) result = g_string_append_c(result, *p);
793                         }
794                 else if (*p == '\'')
795                         {
796                         if (output) result = g_string_append_c(result, *p);
797                         if (!single_quotes && !double_quotes)
798                                 single_quotes = TRUE;
799                         else if (single_quotes)
800                                 single_quotes = FALSE;
801                         }
802                 else if (*p == '"')
803                         {
804                         if (output) result = g_string_append_c(result, *p);
805                         if (!single_quotes && !double_quotes)
806                                 double_quotes = TRUE;
807                         else if (double_quotes)
808                                 double_quotes = FALSE;
809                         }
810                 else if (*p == '%' && p[1])
811                         {
812                         gchar *pathl = NULL;
813
814                         p++;
815
816                         switch (*p)
817                                 {
818                                 case 'f': /* single file */
819                                 case 'u': /* single url */
820                                         flags |= EDITOR_FOR_EACH;
821                                         if (flags & EDITOR_SINGLE_COMMAND)
822                                                 {
823                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
824                                                 goto err;
825                                                 }
826                                         if (list)
827                                                 {
828                                                 /* use the first file from the list */
829                                                 if (!list->data)
830                                                         {
831                                                         flags |= EDITOR_ERROR_NO_FILE;
832                                                         goto err;
833                                                         }
834                                                 pathl = editor_command_path_parse((FileData *)list->data,
835                                                                                   consider_sidecars,
836                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
837                                                                                   editor);
838                                                 if (!output)
839                                                         {
840                                                         /* just testing, check also the rest of the list (like with F and U)
841                                                            any matching file is OK */
842                                                         GList *work = list->next;
843
844                                                         while (!pathl && work)
845                                                                 {
846                                                                 FileData *fd = work->data;
847                                                                 pathl = editor_command_path_parse(fd,
848                                                                                                   consider_sidecars,
849                                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
850                                                                                                   editor);
851                                                                 work = work->next;
852                                                                 }
853                                                         }
854
855                                                 if (!pathl)
856                                                         {
857                                                         flags |= EDITOR_ERROR_NO_FILE;
858                                                         goto err;
859                                                         }
860                                                 if (output)
861                                                         {
862                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
863                                                         }
864                                                 g_free(pathl);
865                                                 }
866                                         break;
867
868                                 case 'F':
869                                 case 'U':
870                                         flags |= EDITOR_SINGLE_COMMAND;
871                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
872                                                 {
873                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
874                                                 goto err;
875                                                 }
876
877                                         if (list)
878                                                 {
879                                                 /* use whole list */
880                                                 GList *work = list;
881                                                 gboolean ok = FALSE;
882
883                                                 while (work)
884                                                         {
885                                                         FileData *fd = work->data;
886                                                         pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
887                                                         if (pathl)
888                                                                 {
889                                                                 ok = TRUE;
890
891                                                                 if (output)
892                                                                         {
893                                                                         ok = TRUE;
894                                                                         if (work != list) g_string_append_c(result, ' ');
895                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
896                                                                         }
897                                                                 g_free(pathl);
898                                                                 }
899                                                         work = work->next;
900                                                         }
901                                                 if (!ok)
902                                                         {
903                                                         flags |= EDITOR_ERROR_NO_FILE;
904                                                         goto err;
905                                                         }
906                                                 }
907                                         break;
908                                 case 'i':
909                                         if (editor->icon && *editor->icon)
910                                                 {
911                                                 if (output)
912                                                         {
913                                                         result = g_string_append(result, "--icon ");
914                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
915                                                         }
916                                                 }
917                                         break;
918                                 case 'c':
919                                         if (output)
920                                                 {
921                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
922                                                 }
923                                         break;
924                                 case 'k':
925                                         if (output)
926                                                 {
927                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
928                                                 }
929                                         break;
930                                 case '%':
931                                         /* %% = % escaping */
932                                         if (output) result = g_string_append_c(result, *p);
933                                         break;
934                                 case 'd':
935                                 case 'D':
936                                 case 'n':
937                                 case 'N':
938                                 case 'v':
939                                 case 'm':
940                                         /* deprecated according to spec, ignore */
941                                         break;
942                                 default:
943                                         flags |= EDITOR_ERROR_SYNTAX;
944                                         goto err;
945                                 }
946                         }
947                 else
948                         {
949                         if (output) result = g_string_append_c(result, *p);
950                         }
951                 p++;
952                 }
953
954         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;
955
956         if (output)
957                 {
958                 *output = g_string_free(result, FALSE);
959                 DEBUG_3("Editor cmd: %s", *output);
960                 }
961
962         return flags;
963
964
965 err:
966         if (output)
967                 {
968                 g_string_free(result, TRUE);
969                 *output = NULL;
970                 }
971         return flags;
972 }
973
974
975 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
976 {
977         EditorData *ed = data;
978         g_spawn_close_pid(pid);
979         ed->pid = -1;
980
981         editor_command_next_finish(ed, status);
982 }
983
984
985 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
986 {
987         gchar *command;
988         FileData *fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : list->data;;
989         GPid pid;
990         gint standard_output;
991         gint standard_error;
992         gboolean ok;
993
994         ed->pid = -1;
995         ed->flags = editor->flags;
996         ed->flags |= editor_command_parse(editor, list, TRUE, &command);
997
998         ok = !EDITOR_ERRORS(ed->flags);
999
1000         if (ok)
1001                 {
1002                 ok = (options->shell.path && *options->shell.path);
1003                 if (!ok) log_printf("ERROR: empty shell command\n");
1004
1005                 if (ok)
1006                         {
1007                         ok = (access(options->shell.path, X_OK) == 0);
1008                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1009                         }
1010
1011                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
1012                 }
1013
1014         if (ok)
1015                 {
1016                 gchar *working_directory;
1017                 gchar *args[4];
1018                 guint n = 0;
1019
1020                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1021                 args[n++] = options->shell.path;
1022                 if (options->shell.options && *options->shell.options)
1023                         args[n++] = options->shell.options;
1024                 args[n++] = command;
1025                 args[n] = NULL;
1026
1027                 if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
1028                         {
1029                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1030                         }
1031                 else
1032                         {
1033                         g_unsetenv("GEEQIE_DESTINATION");
1034                         }
1035
1036                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
1037                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1038                                       NULL, NULL,
1039                                       &pid,
1040                                       NULL,
1041                                       ed->vd ? &standard_output : NULL,
1042                                       ed->vd ? &standard_error : NULL,
1043                                       NULL);
1044
1045                 g_free(working_directory);
1046
1047                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
1048                 }
1049
1050         if (ok)
1051                 {
1052                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1053                 ed->pid = pid;
1054                 }
1055
1056         if (ed->vd)
1057                 {
1058                 if (!ok)
1059                         {
1060                         gchar *buf;
1061
1062                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1063                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1064                         g_free(buf);
1065
1066                         }
1067                 else
1068                         {
1069                         GIOChannel *channel_output;
1070                         GIOChannel *channel_error;
1071
1072                         channel_output = g_io_channel_unix_new(standard_output);
1073                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1074                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1075
1076                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1077                                             editor_verbose_io_cb, ed, NULL);
1078                         g_io_channel_unref(channel_output);
1079
1080                         channel_error = g_io_channel_unix_new(standard_error);
1081                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1082                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1083
1084                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1085                                             editor_verbose_io_cb, ed, NULL);
1086                         g_io_channel_unref(channel_error);
1087                         }
1088                 }
1089
1090         g_free(command);
1091
1092         return EDITOR_ERRORS(ed->flags);
1093 }
1094
1095 static EditorFlags editor_command_next_start(EditorData *ed)
1096 {
1097         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1098
1099         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1100                 {
1101                 FileData *fd;
1102                 EditorFlags error;
1103
1104                 fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;
1105
1106                 if (ed->vd)
1107                         {
1108                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1109                                 editor_verbose_window_progress(ed, fd->path);
1110                         else
1111                                 editor_verbose_window_progress(ed, _("running..."));
1112                         }
1113                 ed->count++;
1114
1115                 error = editor_command_one(ed->editor, ed->list, ed);
1116                 if (!error && ed->vd)
1117                         {
1118                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1119                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1120                                 {
1121                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1122                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1123                                 }
1124                         }
1125
1126                 if (!error)
1127                         return 0;
1128
1129                 /* command was not started, call the finish immediately */
1130                 return editor_command_next_finish(ed, 0);
1131                 }
1132
1133         /* everything is done */
1134         return editor_command_done(ed);
1135 }
1136
1137 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1138 {
1139         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1140
1141         if (status)
1142                 ed->flags |= EDITOR_ERROR_STATUS;
1143
1144         if (ed->flags & EDITOR_FOR_EACH)
1145                 {
1146                 /* handle the first element from the list */
1147                 GList *fd_element = ed->list;
1148
1149                 ed->list = g_list_remove_link(ed->list, fd_element);
1150                 if (ed->callback)
1151                         {
1152                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1153                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1154                         }
1155                 filelist_free(fd_element);
1156                 }
1157         else
1158                 {
1159                 /* handle whole list */
1160                 if (ed->callback)
1161                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1162                 filelist_free(ed->list);
1163                 ed->list = NULL;
1164                 }
1165
1166         switch (cont)
1167                 {
1168                 case EDITOR_CB_SUSPEND:
1169                         return EDITOR_ERRORS(ed->flags);
1170                 case EDITOR_CB_SKIP:
1171                         return editor_command_done(ed);
1172                 }
1173
1174         return editor_command_next_start(ed);
1175 }
1176
1177 static EditorFlags editor_command_done(EditorData *ed)
1178 {
1179         EditorFlags flags;
1180
1181         if (ed->vd)
1182                 {
1183                 if (ed->count == ed->total)
1184                         {
1185                         editor_verbose_window_progress(ed, _("done"));
1186                         }
1187                 else
1188                         {
1189                         editor_verbose_window_progress(ed, _("stopped by user"));
1190                         }
1191                 editor_verbose_window_enable_close(ed->vd);
1192                 }
1193
1194         /* free the not-handled items */
1195         if (ed->list)
1196                 {
1197                 ed->flags |= EDITOR_ERROR_SKIPPED;
1198                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1199                 filelist_free(ed->list);
1200                 ed->list = NULL;
1201                 }
1202
1203         ed->count = 0;
1204
1205         flags = EDITOR_ERRORS(ed->flags);
1206
1207         if (!ed->vd) editor_data_free(ed);
1208
1209         return flags;
1210 }
1211
1212 void editor_resume(gpointer ed)
1213 {
1214         editor_command_next_start(ed);
1215 }
1216
1217 void editor_skip(gpointer ed)
1218 {
1219         editor_command_done(ed);
1220 }
1221
1222 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1223 {
1224         EditorData *ed;
1225         EditorFlags flags = editor->flags;
1226
1227         if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);
1228
1229         ed = g_new0(EditorData, 1);
1230         ed->list = filelist_copy(list);
1231         ed->flags = flags;
1232         ed->editor = editor;
1233         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1234         ed->callback = cb;
1235         ed->data = data;
1236         ed->working_directory = g_strdup(working_directory);
1237
1238         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1239                 flags |= EDITOR_VERBOSE;
1240
1241         if (flags & EDITOR_VERBOSE)
1242                 editor_verbose_window(ed, text);
1243
1244         editor_command_next_start(ed);
1245         /* errors from editor_command_next_start will be handled via callback */
1246         return EDITOR_ERRORS(flags);
1247 }
1248
1249 gboolean is_valid_editor_command(const gchar *key)
1250 {
1251         if (!key) return FALSE;
1252         return g_hash_table_lookup(editors, key) != NULL;
1253 }
1254
1255 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1256 {
1257         EditorFlags error;
1258         EditorDescription *editor;
1259         if (!key) return EDITOR_ERROR_EMPTY;
1260
1261         editor = g_hash_table_lookup(editors, key);
1262
1263         if (!editor) return EDITOR_ERROR_EMPTY;
1264         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1265
1266         error = editor_command_parse(editor, list, TRUE, NULL);
1267
1268         if (EDITOR_ERRORS(error)) return error;
1269
1270         error |= editor_command_start(editor, editor->name, list, working_directory, cb, data);
1271
1272         if (EDITOR_ERRORS(error))
1273                 {
1274                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1275
1276                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1277                 g_free(text);
1278                 }
1279
1280         return EDITOR_ERRORS(error);
1281 }
1282
1283 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1284 {
1285         return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
1286 }
1287
1288 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1289 {
1290         GList *list;
1291         EditorFlags error;
1292
1293         if (!fd) return FALSE;
1294
1295         list = g_list_append(NULL, fd);
1296         error = start_editor_from_filelist_full(key, list, NULL, cb, data);
1297         g_list_free(list);
1298         return error;
1299 }
1300
1301 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1302 {
1303         return start_editor_from_file_full(key, fd, NULL, NULL);
1304 }
1305
1306 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1307 {
1308         return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
1309 }
1310
1311 gboolean editor_window_flag_set(const gchar *key)
1312 {
1313         EditorDescription *editor;
1314         if (!key) return TRUE;
1315
1316         editor = g_hash_table_lookup(editors, key);
1317         if (!editor) return TRUE;
1318
1319         return !!(editor->flags & EDITOR_KEEP_FS);
1320 }
1321
1322 gboolean editor_is_filter(const gchar *key)
1323 {
1324         EditorDescription *editor;
1325         if (!key) return TRUE;
1326
1327         editor = g_hash_table_lookup(editors, key);
1328         if (!editor) return TRUE;
1329
1330         return !!(editor->flags & EDITOR_DEST);
1331 }
1332
1333 gboolean editor_no_param(const gchar *key)
1334 {
1335         EditorDescription *editor;
1336         if (!key) return FALSE;
1337
1338         editor = g_hash_table_lookup(editors, key);
1339         if (!editor) return FALSE;
1340
1341         return !!(editor->flags & EDITOR_NO_PARAM);
1342 }
1343
1344 gboolean editor_blocks_file(const gchar *key)
1345 {
1346         EditorDescription *editor;
1347         if (!key) return FALSE;
1348
1349         editor = g_hash_table_lookup(editors, key);
1350         if (!editor) return FALSE;
1351
1352         /* Decide if the image file should be blocked during editor execution
1353            Editors like gimp can be used long time after the original file was
1354            saved, for editing unrelated files.
1355            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1356         */
1357
1358         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1359 }
1360
1361 const gchar *editor_get_error_str(EditorFlags flags)
1362 {
1363         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1364         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1365         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1366         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1367         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1368         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1369         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1370         return _("Unknown error.");
1371 }
1372
1373 const gchar *editor_get_name(const gchar *key)
1374 {
1375         EditorDescription *editor = g_hash_table_lookup(editors, key);
1376
1377         if (!editor) return NULL;
1378
1379         return editor->name;
1380 }
1381 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */