Update translation files after more source file shuffling.
[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 #if GTK_CHECK_VERSION(3,0,0)
570         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
571         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
572 #endif
573         gtk_widget_show(vd->progress);
574
575         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
576         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
577         gtk_widget_show(vd->spinner);
578
579         gtk_widget_show(vd->gd->dialog);
580
581         ed->vd = vd;
582         return vd;
583 }
584
585 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
586 {
587         GtkTextBuffer *buffer;
588         GtkTextIter iter;
589
590         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
591         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
592         gtk_text_buffer_insert(buffer, &iter, text, len);
593 }
594
595 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
596 {
597         if (!ed->vd) return;
598
599         if (ed->total)
600                 {
601                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
602                 }
603
604         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
605 }
606
607 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
608 {
609         EditorData *ed = data;
610         gchar buf[512];
611         gsize count;
612
613         if (condition & G_IO_IN)
614                 {
615                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
616                         {
617                         if (!g_utf8_validate(buf, count, NULL))
618                                 {
619                                 gchar *utf8;
620
621                                 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
622                                 if (utf8)
623                                         {
624                                         editor_verbose_window_fill(ed->vd, utf8, -1);
625                                         g_free(utf8);
626                                         }
627                                 else
628                                         {
629                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
630                                         }
631                                 }
632                         else
633                                 {
634                                 editor_verbose_window_fill(ed->vd, buf, count);
635                                 }
636                         }
637                 }
638
639         if (condition & (G_IO_ERR | G_IO_HUP))
640                 {
641                 g_io_channel_shutdown(source, TRUE, NULL);
642                 return FALSE;
643                 }
644
645         return TRUE;
646 }
647
648 typedef enum {
649         PATH_FILE,
650         PATH_FILE_URL,
651         PATH_DEST
652 } PathType;
653
654
655 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
656 {
657         GString *string;
658         gchar *pathl;
659         const gchar *p = NULL;
660
661         DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
662
663         string = g_string_new("");
664
665         if (type == PATH_FILE || type == PATH_FILE_URL)
666                 {
667                 GList *work = editor->ext_list;
668
669                 if (!work)
670                         p = fd->path;
671                 else
672                         {
673                         while (work)
674                                 {
675                                 GList *work2;
676                                 gchar *ext = work->data;
677                                 work = work->next;
678
679                                 if (strcmp(ext, "*") == 0 ||
680                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
681                                         {
682                                         p = fd->path;
683                                         break;
684                                         }
685
686                                 work2 = consider_sidecars ? fd->sidecar_files : NULL;
687                                 while (work2)
688                                         {
689                                         FileData *sfd = work2->data;
690                                         work2 = work2->next;
691
692                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
693                                                 {
694                                                 p = sfd->path;
695                                                 break;
696                                                 }
697                                         }
698                                 if (p) break;
699                                 }
700                         if (!p) return NULL;
701                         }
702                 }
703         else if (type == PATH_DEST)
704                 {
705                 if (fd->change && fd->change->dest)
706                         p = fd->change->dest;
707                 else
708                         p = "";
709                 }
710
711         g_assert(p);
712         string = g_string_append(string, p);
713
714         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
715         pathl = path_from_utf8(string->str);
716         g_string_free(string, TRUE);
717
718         if (pathl && !pathl[0]) /* empty string case */
719                 {
720                 g_free(pathl);
721                 pathl = NULL;
722                 }
723
724         DEBUG_2("editor_command_path_parse: return %s", pathl);
725         return pathl;
726 }
727
728 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
729 {
730         const char *p;
731
732         if (!single_quotes)
733                 {
734                 if (!double_quotes)
735                         g_string_append_c(str, '\'');
736                 else
737                         g_string_append(str, "\"'");
738                 }
739
740         for (p = s; *p != '\0'; p++)
741                 {
742                 if (*p == '\'')
743                         g_string_append(str, "'\\''");
744                 else
745                         g_string_append_c(str, *p);
746                 }
747
748         if (!single_quotes)
749                 {
750                 if (!double_quotes)
751                         g_string_append_c(str, '\'');
752                 else
753                         g_string_append(str, "'\"");
754                 }
755
756         return str;
757 }
758
759
760 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
761 {
762         EditorFlags flags = 0;
763         const gchar *p;
764         GString *result = NULL;
765         gboolean escape = FALSE;
766         gboolean single_quotes = FALSE;
767         gboolean double_quotes = FALSE;
768
769         DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
770
771         if (output)
772                 result = g_string_new("");
773
774         if (editor->exec == NULL || editor->exec[0] == '\0')
775                 {
776                 flags |= EDITOR_ERROR_EMPTY;
777                 goto err;
778                 }
779
780         p = editor->exec;
781         /* skip leading whitespaces if any */
782         while (g_ascii_isspace(*p)) p++;
783
784         /* command */
785
786         while (*p)
787                 {
788                 if (escape)
789                         {
790                         escape = FALSE;
791                         if (output) result = g_string_append_c(result, *p);
792                         }
793                 else if (*p == '\\')
794                         {
795                         if (!single_quotes) escape = TRUE;
796                         if (output) result = g_string_append_c(result, *p);
797                         }
798                 else if (*p == '\'')
799                         {
800                         if (output) result = g_string_append_c(result, *p);
801                         if (!single_quotes && !double_quotes)
802                                 single_quotes = TRUE;
803                         else if (single_quotes)
804                                 single_quotes = FALSE;
805                         }
806                 else if (*p == '"')
807                         {
808                         if (output) result = g_string_append_c(result, *p);
809                         if (!single_quotes && !double_quotes)
810                                 double_quotes = TRUE;
811                         else if (double_quotes)
812                                 double_quotes = FALSE;
813                         }
814                 else if (*p == '%' && p[1])
815                         {
816                         gchar *pathl = NULL;
817
818                         p++;
819
820                         switch (*p)
821                                 {
822                                 case 'f': /* single file */
823                                 case 'u': /* single url */
824                                         flags |= EDITOR_FOR_EACH;
825                                         if (flags & EDITOR_SINGLE_COMMAND)
826                                                 {
827                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
828                                                 goto err;
829                                                 }
830                                         if (list)
831                                                 {
832                                                 /* use the first file from the list */
833                                                 if (!list->data)
834                                                         {
835                                                         flags |= EDITOR_ERROR_NO_FILE;
836                                                         goto err;
837                                                         }
838                                                 pathl = editor_command_path_parse((FileData *)list->data,
839                                                                                   consider_sidecars,
840                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
841                                                                                   editor);
842                                                 if (!output)
843                                                         {
844                                                         /* just testing, check also the rest of the list (like with F and U)
845                                                            any matching file is OK */
846                                                         GList *work = list->next;
847
848                                                         while (!pathl && work)
849                                                                 {
850                                                                 FileData *fd = work->data;
851                                                                 pathl = editor_command_path_parse(fd,
852                                                                                                   consider_sidecars,
853                                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
854                                                                                                   editor);
855                                                                 work = work->next;
856                                                                 }
857                                                         }
858
859                                                 if (!pathl)
860                                                         {
861                                                         flags |= EDITOR_ERROR_NO_FILE;
862                                                         goto err;
863                                                         }
864                                                 if (output)
865                                                         {
866                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
867                                                         }
868                                                 g_free(pathl);
869                                                 }
870                                         break;
871
872                                 case 'F':
873                                 case 'U':
874                                         flags |= EDITOR_SINGLE_COMMAND;
875                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
876                                                 {
877                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
878                                                 goto err;
879                                                 }
880
881                                         if (list)
882                                                 {
883                                                 /* use whole list */
884                                                 GList *work = list;
885                                                 gboolean ok = FALSE;
886
887                                                 while (work)
888                                                         {
889                                                         FileData *fd = work->data;
890                                                         pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
891                                                         if (pathl)
892                                                                 {
893                                                                 ok = TRUE;
894
895                                                                 if (output)
896                                                                         {
897                                                                         ok = TRUE;
898                                                                         if (work != list) g_string_append_c(result, ' ');
899                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
900                                                                         }
901                                                                 g_free(pathl);
902                                                                 }
903                                                         work = work->next;
904                                                         }
905                                                 if (!ok)
906                                                         {
907                                                         flags |= EDITOR_ERROR_NO_FILE;
908                                                         goto err;
909                                                         }
910                                                 }
911                                         break;
912                                 case 'i':
913                                         if (editor->icon && *editor->icon)
914                                                 {
915                                                 if (output)
916                                                         {
917                                                         result = g_string_append(result, "--icon ");
918                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
919                                                         }
920                                                 }
921                                         break;
922                                 case 'c':
923                                         if (output)
924                                                 {
925                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
926                                                 }
927                                         break;
928                                 case 'k':
929                                         if (output)
930                                                 {
931                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
932                                                 }
933                                         break;
934                                 case '%':
935                                         /* %% = % escaping */
936                                         if (output) result = g_string_append_c(result, *p);
937                                         break;
938                                 case 'd':
939                                 case 'D':
940                                 case 'n':
941                                 case 'N':
942                                 case 'v':
943                                 case 'm':
944                                         /* deprecated according to spec, ignore */
945                                         break;
946                                 default:
947                                         flags |= EDITOR_ERROR_SYNTAX;
948                                         goto err;
949                                 }
950                         }
951                 else
952                         {
953                         if (output) result = g_string_append_c(result, *p);
954                         }
955                 p++;
956                 }
957
958         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;
959
960         if (output)
961                 {
962                 *output = g_string_free(result, FALSE);
963                 DEBUG_3("Editor cmd: %s", *output);
964                 }
965
966         return flags;
967
968
969 err:
970         if (output)
971                 {
972                 g_string_free(result, TRUE);
973                 *output = NULL;
974                 }
975         return flags;
976 }
977
978
979 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
980 {
981         EditorData *ed = data;
982         g_spawn_close_pid(pid);
983         ed->pid = -1;
984
985         editor_command_next_finish(ed, status);
986 }
987
988
989 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
990 {
991         gchar *command;
992         FileData *fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : list->data;;
993         GPid pid;
994         gint standard_output;
995         gint standard_error;
996         gboolean ok;
997
998         ed->pid = -1;
999         ed->flags = editor->flags;
1000         ed->flags |= editor_command_parse(editor, list, TRUE, &command);
1001
1002         ok = !EDITOR_ERRORS(ed->flags);
1003
1004         if (ok)
1005                 {
1006                 ok = (options->shell.path && *options->shell.path);
1007                 if (!ok) log_printf("ERROR: empty shell command\n");
1008
1009                 if (ok)
1010                         {
1011                         ok = (access(options->shell.path, X_OK) == 0);
1012                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1013                         }
1014
1015                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
1016                 }
1017
1018         if (ok)
1019                 {
1020                 gchar *working_directory;
1021                 gchar *args[4];
1022                 guint n = 0;
1023
1024                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1025                 args[n++] = options->shell.path;
1026                 if (options->shell.options && *options->shell.options)
1027                         args[n++] = options->shell.options;
1028                 args[n++] = command;
1029                 args[n] = NULL;
1030
1031                 if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
1032                         {
1033                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1034                         }
1035                 else
1036                         {
1037                         g_unsetenv("GEEQIE_DESTINATION");
1038                         }
1039
1040                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
1041                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1042                                       NULL, NULL,
1043                                       &pid,
1044                                       NULL,
1045                                       ed->vd ? &standard_output : NULL,
1046                                       ed->vd ? &standard_error : NULL,
1047                                       NULL);
1048
1049                 g_free(working_directory);
1050
1051                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
1052                 }
1053
1054         if (ok)
1055                 {
1056                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1057                 ed->pid = pid;
1058                 }
1059
1060         if (ed->vd)
1061                 {
1062                 if (!ok)
1063                         {
1064                         gchar *buf;
1065
1066                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1067                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1068                         g_free(buf);
1069
1070                         }
1071                 else
1072                         {
1073                         GIOChannel *channel_output;
1074                         GIOChannel *channel_error;
1075
1076                         channel_output = g_io_channel_unix_new(standard_output);
1077                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1078                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1079
1080                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1081                                             editor_verbose_io_cb, ed, NULL);
1082                         g_io_channel_unref(channel_output);
1083
1084                         channel_error = g_io_channel_unix_new(standard_error);
1085                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1086                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1087
1088                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1089                                             editor_verbose_io_cb, ed, NULL);
1090                         g_io_channel_unref(channel_error);
1091                         }
1092                 }
1093
1094         g_free(command);
1095
1096         return EDITOR_ERRORS(ed->flags);
1097 }
1098
1099 static EditorFlags editor_command_next_start(EditorData *ed)
1100 {
1101         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1102
1103         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1104                 {
1105                 FileData *fd;
1106                 EditorFlags error;
1107
1108                 fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;
1109
1110                 if (ed->vd)
1111                         {
1112                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1113                                 editor_verbose_window_progress(ed, fd->path);
1114                         else
1115                                 editor_verbose_window_progress(ed, _("running..."));
1116                         }
1117                 ed->count++;
1118
1119                 error = editor_command_one(ed->editor, ed->list, ed);
1120                 if (!error && ed->vd)
1121                         {
1122                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1123                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1124                                 {
1125                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1126                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1127                                 }
1128                         }
1129
1130                 if (!error)
1131                         return 0;
1132
1133                 /* command was not started, call the finish immediately */
1134                 return editor_command_next_finish(ed, 0);
1135                 }
1136
1137         /* everything is done */
1138         return editor_command_done(ed);
1139 }
1140
1141 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1142 {
1143         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1144
1145         if (status)
1146                 ed->flags |= EDITOR_ERROR_STATUS;
1147
1148         if (ed->flags & EDITOR_FOR_EACH)
1149                 {
1150                 /* handle the first element from the list */
1151                 GList *fd_element = ed->list;
1152
1153                 ed->list = g_list_remove_link(ed->list, fd_element);
1154                 if (ed->callback)
1155                         {
1156                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1157                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1158                         }
1159                 filelist_free(fd_element);
1160                 }
1161         else
1162                 {
1163                 /* handle whole list */
1164                 if (ed->callback)
1165                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1166                 filelist_free(ed->list);
1167                 ed->list = NULL;
1168                 }
1169
1170         switch (cont)
1171                 {
1172                 case EDITOR_CB_SUSPEND:
1173                         return EDITOR_ERRORS(ed->flags);
1174                 case EDITOR_CB_SKIP:
1175                         return editor_command_done(ed);
1176                 }
1177
1178         return editor_command_next_start(ed);
1179 }
1180
1181 static EditorFlags editor_command_done(EditorData *ed)
1182 {
1183         EditorFlags flags;
1184
1185         if (ed->vd)
1186                 {
1187                 if (ed->count == ed->total)
1188                         {
1189                         editor_verbose_window_progress(ed, _("done"));
1190                         }
1191                 else
1192                         {
1193                         editor_verbose_window_progress(ed, _("stopped by user"));
1194                         }
1195                 editor_verbose_window_enable_close(ed->vd);
1196                 }
1197
1198         /* free the not-handled items */
1199         if (ed->list)
1200                 {
1201                 ed->flags |= EDITOR_ERROR_SKIPPED;
1202                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1203                 filelist_free(ed->list);
1204                 ed->list = NULL;
1205                 }
1206
1207         ed->count = 0;
1208
1209         flags = EDITOR_ERRORS(ed->flags);
1210
1211         if (!ed->vd) editor_data_free(ed);
1212
1213         return flags;
1214 }
1215
1216 void editor_resume(gpointer ed)
1217 {
1218         editor_command_next_start(ed);
1219 }
1220
1221 void editor_skip(gpointer ed)
1222 {
1223         editor_command_done(ed);
1224 }
1225
1226 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1227 {
1228         EditorData *ed;
1229         EditorFlags flags = editor->flags;
1230
1231         if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);
1232
1233         ed = g_new0(EditorData, 1);
1234         ed->list = filelist_copy(list);
1235         ed->flags = flags;
1236         ed->editor = editor;
1237         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1238         ed->callback = cb;
1239         ed->data = data;
1240         ed->working_directory = g_strdup(working_directory);
1241
1242         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1243                 flags |= EDITOR_VERBOSE;
1244
1245         if (flags & EDITOR_VERBOSE)
1246                 editor_verbose_window(ed, text);
1247
1248         editor_command_next_start(ed);
1249         /* errors from editor_command_next_start will be handled via callback */
1250         return EDITOR_ERRORS(flags);
1251 }
1252
1253 gboolean is_valid_editor_command(const gchar *key)
1254 {
1255         if (!key) return FALSE;
1256         return g_hash_table_lookup(editors, key) != NULL;
1257 }
1258
1259 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1260 {
1261         EditorFlags error;
1262         EditorDescription *editor;
1263         if (!key) return EDITOR_ERROR_EMPTY;
1264
1265         editor = g_hash_table_lookup(editors, key);
1266
1267         if (!editor) return EDITOR_ERROR_EMPTY;
1268         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1269
1270         error = editor_command_parse(editor, list, TRUE, NULL);
1271
1272         if (EDITOR_ERRORS(error)) return error;
1273
1274         error |= editor_command_start(editor, editor->name, list, working_directory, cb, data);
1275
1276         if (EDITOR_ERRORS(error))
1277                 {
1278                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1279
1280                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1281                 g_free(text);
1282                 }
1283
1284         return EDITOR_ERRORS(error);
1285 }
1286
1287 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1288 {
1289         return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
1290 }
1291
1292 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1293 {
1294         GList *list;
1295         EditorFlags error;
1296
1297         if (!fd) return FALSE;
1298
1299         list = g_list_append(NULL, fd);
1300         error = start_editor_from_filelist_full(key, list, NULL, cb, data);
1301         g_list_free(list);
1302         return error;
1303 }
1304
1305 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1306 {
1307         return start_editor_from_file_full(key, fd, NULL, NULL);
1308 }
1309
1310 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1311 {
1312         return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
1313 }
1314
1315 gboolean editor_window_flag_set(const gchar *key)
1316 {
1317         EditorDescription *editor;
1318         if (!key) return TRUE;
1319
1320         editor = g_hash_table_lookup(editors, key);
1321         if (!editor) return TRUE;
1322
1323         return !!(editor->flags & EDITOR_KEEP_FS);
1324 }
1325
1326 gboolean editor_is_filter(const gchar *key)
1327 {
1328         EditorDescription *editor;
1329         if (!key) return TRUE;
1330
1331         editor = g_hash_table_lookup(editors, key);
1332         if (!editor) return TRUE;
1333
1334         return !!(editor->flags & EDITOR_DEST);
1335 }
1336
1337 gboolean editor_no_param(const gchar *key)
1338 {
1339         EditorDescription *editor;
1340         if (!key) return FALSE;
1341
1342         editor = g_hash_table_lookup(editors, key);
1343         if (!editor) return FALSE;
1344
1345         return !!(editor->flags & EDITOR_NO_PARAM);
1346 }
1347
1348 gboolean editor_blocks_file(const gchar *key)
1349 {
1350         EditorDescription *editor;
1351         if (!key) return FALSE;
1352
1353         editor = g_hash_table_lookup(editors, key);
1354         if (!editor) return FALSE;
1355
1356         /* Decide if the image file should be blocked during editor execution
1357            Editors like gimp can be used long time after the original file was
1358            saved, for editing unrelated files.
1359            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1360         */
1361
1362         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1363 }
1364
1365 const gchar *editor_get_error_str(EditorFlags flags)
1366 {
1367         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1368         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1369         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1370         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1371         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1372         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1373         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1374         return _("Unknown error.");
1375 }
1376
1377 const gchar *editor_get_name(const gchar *key)
1378 {
1379         EditorDescription *editor = g_hash_table_lookup(editors, key);
1380
1381         if (!editor) return NULL;
1382
1383         return editor->name;
1384 }
1385 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */