Mercurial > geeqie
annotate src/editors.c @ 737:8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
This allows users to modify the shell command that execute "editors".
Two new options appear in rc file:
- shell.path (default to "/bin/sh")
- shell.options (default to "-c")
These options can only be changed from the rc file, not at runtime.
Tests are made to check that shell.path is not empty and lead to
an executable file.
author | zas_ |
---|---|
date | Thu, 22 May 2008 20:22:13 +0000 |
parents | fa8f7d7396cf |
children | 477f48ba28d8 |
rev | line source |
---|---|
9 | 1 /* |
196 | 2 * Geeqie |
123
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
3 * (C) 2006 John Ellis |
475 | 4 * Copyright (C) 2008 The Geeqie Team |
9 | 5 * |
6 * Author: John Ellis | |
7 * | |
8 * This software is released under the GNU General Public License (GNU GPL). | |
9 * Please read the included file COPYING for more information. | |
10 * This software comes with no warranty of any kind, use at your own risk! | |
11 */ | |
12 | |
13 | |
281 | 14 #include "main.h" |
9 | 15 #include "editors.h" |
16 | |
669 | 17 #include "filedata.h" |
18 #include "filefilter.h" | |
9 | 19 #include "utilops.h" |
20 #include "ui_fileops.h" | |
21 #include "ui_spinner.h" | |
22 #include "ui_utildlg.h" | |
23 | |
24 #include <errno.h> | |
25 | |
26 | |
27 #define EDITOR_WINDOW_WIDTH 500 | |
28 #define EDITOR_WINDOW_HEIGHT 300 | |
29 | |
30 | |
31 | |
32 typedef struct _EditorVerboseData EditorVerboseData; | |
33 struct _EditorVerboseData { | |
34 GenericDialog *gd; | |
35 GtkWidget *button_close; | |
36 GtkWidget *button_stop; | |
37 GtkWidget *text; | |
38 GtkWidget *progress; | |
39 GtkWidget *spinner; | |
140 | 40 }; |
41 | |
42 typedef struct _EditorData EditorData; | |
43 struct _EditorData { | |
44 gint flags; | |
45 GPid pid; | |
46 gchar *command_template; | |
47 GList *list; | |
9 | 48 gint count; |
49 gint total; | |
140 | 50 gboolean stopping; |
51 EditorVerboseData *vd; | |
52 EditorCallback callback; | |
53 gpointer data; | |
9 | 54 }; |
55 | |
56 | |
730 | 57 static Editor editor_slot_defaults[GQ_EDITOR_SLOTS] = { |
58 { N_("The Gimp"), "gimp-remote %{.cr2;.crw;.nef;.raw;*}f" }, | |
59 { N_("XV"), "xv %f" }, | |
60 { N_("Xpaint"), "xpaint %f" }, | |
61 { N_("UFraw"), "ufraw %{.cr2;.crw;.nef;.raw}p" }, | |
62 { N_("Add XMP sidecar"), "%vFILE=%{.cr2;.crw;.nef;.raw}p;XMP=`echo \"$FILE\"|sed -e 's|\\.[^.]*$|.xmp|'`; exiftool -tagsfromfile \"$FILE\" \"$XMP\"" }, | |
63 { NULL, NULL }, | |
64 { NULL, NULL }, | |
65 { NULL, NULL }, | |
66 { N_("Rotate jpeg clockwise"), "%vif jpegtran -rotate 90 -copy all -outfile %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p; then mv %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p;else rm %{.jpg;.jpeg}p_tmp;fi" }, | |
67 { N_("Rotate jpeg counterclockwise"), "%vif jpegtran -rotate 270 -copy all -outfile %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p; then mv %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p;else rm %{.jpg;.jpeg}p_tmp;fi" }, | |
134
9009856628f7
started implementation of external commands; external Delete should work
nadvornik
parents:
123
diff
changeset
|
68 /* special slots */ |
136 | 69 #if 1 |
70 /* for testing */ | |
730 | 71 { N_("External Copy command"), "%vset -x;cp %p %d" }, |
72 { N_("External Move command"), "%vset -x;mv %p %d" }, | |
73 { N_("External Rename command"), "%vset -x;mv %p %d" }, | |
74 { N_("External Delete command"), NULL }, | |
75 { N_("External New Folder command"), NULL }, | |
136 | 76 #else |
730 | 77 { N_("External Copy command"), NULL }, |
78 { N_("External Move command"), NULL }, | |
79 { N_("External Rename command"), NULL }, | |
80 { N_("External Delete command"), NULL }, | |
81 { N_("External New Folder command"), NULL }, | |
136 | 82 #endif |
9 | 83 }; |
84 | |
140 | 85 static void editor_verbose_window_progress(EditorData *ed, const gchar *text); |
86 static gint editor_command_next_start(EditorData *ed); | |
87 static gint editor_command_next_finish(EditorData *ed, gint status); | |
88 static gint editor_command_done(EditorData *ed); | |
9 | 89 |
90 /* | |
91 *----------------------------------------------------------------------------- | |
92 * external editor routines | |
93 *----------------------------------------------------------------------------- | |
94 */ | |
95 | |
96 void editor_reset_defaults(void) | |
97 { | |
98 gint i; | |
99 | |
283 | 100 for (i = 0; i < GQ_EDITOR_SLOTS; i++) |
9 | 101 { |
730 | 102 g_free(options->editor[i].name); |
103 options->editor[i].name = g_strdup(_(editor_slot_defaults[i].name)); | |
104 g_free(options->editor[i].command); | |
105 options->editor[i].command = g_strdup(editor_slot_defaults[i].command); | |
9 | 106 } |
107 } | |
108 | |
140 | 109 static void editor_verbose_data_free(EditorData *ed) |
110 { | |
111 if (!ed->vd) return; | |
112 g_free(ed->vd); | |
113 ed->vd = NULL; | |
114 } | |
115 | |
116 static void editor_data_free(EditorData *ed) | |
117 { | |
118 editor_verbose_data_free(ed); | |
119 g_free(ed->command_template); | |
120 g_free(ed); | |
121 } | |
122 | |
9 | 123 static void editor_verbose_window_close(GenericDialog *gd, gpointer data) |
124 { | |
140 | 125 EditorData *ed = data; |
9 | 126 |
127 generic_dialog_close(gd); | |
140 | 128 editor_verbose_data_free(ed); |
129 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */ | |
9 | 130 } |
131 | |
132 static void editor_verbose_window_stop(GenericDialog *gd, gpointer data) | |
133 { | |
140 | 134 EditorData *ed = data; |
135 ed->stopping = TRUE; | |
136 ed->count = 0; | |
137 editor_verbose_window_progress(ed, _("stopping...")); | |
9 | 138 } |
139 | |
140 static void editor_verbose_window_enable_close(EditorVerboseData *vd) | |
141 { | |
142 vd->gd->cancel_cb = editor_verbose_window_close; | |
143 | |
144 spinner_set_interval(vd->spinner, -1); | |
145 gtk_widget_set_sensitive(vd->button_stop, FALSE); | |
146 gtk_widget_set_sensitive(vd->button_close, TRUE); | |
147 } | |
148 | |
140 | 149 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text) |
9 | 150 { |
151 EditorVerboseData *vd; | |
152 GtkWidget *scrolled; | |
153 GtkWidget *hbox; | |
154 gchar *buf; | |
155 | |
156 vd = g_new0(EditorVerboseData, 1); | |
157 | |
254
9faf34f047b1
Make the wmclass value unique among the code by defining
zas_
parents:
238
diff
changeset
|
158 vd->gd = file_util_gen_dlg(_("Edit command results"), GQ_WMCLASS, "editor_results", |
9 | 159 NULL, FALSE, |
140 | 160 NULL, ed); |
9 | 161 buf = g_strdup_printf(_("Output of %s"), text); |
162 generic_dialog_add_message(vd->gd, NULL, buf, NULL); | |
163 g_free(buf); | |
164 vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL, | |
165 editor_verbose_window_stop, FALSE); | |
166 gtk_widget_set_sensitive(vd->button_stop, FALSE); | |
167 vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL, | |
168 editor_verbose_window_close, TRUE); | |
169 gtk_widget_set_sensitive(vd->button_close, FALSE); | |
170 | |
171 scrolled = gtk_scrolled_window_new(NULL, NULL); | |
172 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); | |
173 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), | |
174 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); | |
175 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5); | |
176 gtk_widget_show(scrolled); | |
177 | |
178 vd->text = gtk_text_view_new(); | |
179 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE); | |
180 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT); | |
181 gtk_container_add(GTK_CONTAINER(scrolled), vd->text); | |
182 gtk_widget_show(vd->text); | |
183 | |
184 hbox = gtk_hbox_new(FALSE, 0); | |
185 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0); | |
186 gtk_widget_show(hbox); | |
187 | |
188 vd->progress = gtk_progress_bar_new(); | |
189 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0); | |
190 gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0); | |
191 gtk_widget_show(vd->progress); | |
192 | |
193 vd->spinner = spinner_new(NULL, SPINNER_SPEED); | |
194 gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0); | |
195 gtk_widget_show(vd->spinner); | |
442 | 196 |
9 | 197 gtk_widget_show(vd->gd->dialog); |
198 | |
140 | 199 ed->vd = vd; |
9 | 200 return vd; |
201 } | |
202 | |
203 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len) | |
204 { | |
205 GtkTextBuffer *buffer; | |
206 GtkTextIter iter; | |
207 | |
208 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text)); | |
209 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1); | |
210 gtk_text_buffer_insert(buffer, &iter, text, len); | |
211 } | |
212 | |
140 | 213 static void editor_verbose_window_progress(EditorData *ed, const gchar *text) |
9 | 214 { |
140 | 215 if (!ed->vd) return; |
216 | |
217 if (ed->total) | |
9 | 218 { |
140 | 219 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (double)ed->count / ed->total); |
9 | 220 } |
221 | |
140 | 222 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : ""); |
9 | 223 } |
224 | |
225 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data) | |
226 { | |
140 | 227 EditorData *ed = data; |
9 | 228 gchar buf[512]; |
229 gsize count; | |
230 | |
140 | 231 if (condition & G_IO_IN) |
9 | 232 { |
140 | 233 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL) |
234 { | |
235 if (!g_utf8_validate(buf, count, NULL)) | |
9 | 236 { |
140 | 237 gchar *utf8; |
444 | 238 |
140 | 239 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL); |
240 if (utf8) | |
9 | 241 { |
140 | 242 editor_verbose_window_fill(ed->vd, utf8, -1); |
243 g_free(utf8); | |
9 | 244 } |
245 else | |
246 { | |
288
d1f74154463e
Replace occurences of Geeqie / geeqie by constants defined in main.h.
zas_
parents:
283
diff
changeset
|
247 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1); |
9 | 248 } |
249 } | |
140 | 250 else |
251 { | |
252 editor_verbose_window_fill(ed->vd, buf, count); | |
253 } | |
254 } | |
9 | 255 } |
256 | |
140 | 257 if (condition & (G_IO_ERR | G_IO_HUP)) |
9 | 258 { |
140 | 259 g_io_channel_shutdown(source, TRUE, NULL); |
9 | 260 return FALSE; |
261 } | |
262 | |
263 return TRUE; | |
264 } | |
265 | |
138 | 266 typedef enum { |
267 PATH_FILE, | |
140 | 268 PATH_DEST |
138 | 269 } PathType; |
270 | |
271 | |
140 | 272 static gchar *editor_command_path_parse(const FileData *fd, PathType type, const gchar *extensions) |
123
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
273 { |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
274 GString *string; |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
275 gchar *pathl; |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
276 const gchar *p = NULL; |
123
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
277 |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
278 string = g_string_new(""); |
442 | 279 |
138 | 280 if (type == PATH_FILE) |
281 { | |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
282 GList *ext_list = filter_to_list(extensions); |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
283 GList *work = ext_list; |
442 | 284 |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
285 if (!work) |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
286 p = fd->path; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
287 else |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
288 { |
516 | 289 while (work) |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
290 { |
444 | 291 GList *work2; |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
292 gchar *ext = work->data; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
293 work = work->next; |
442 | 294 |
295 if (strcmp(ext, "*") == 0 || | |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
296 strcasecmp(ext, fd->extension) == 0) |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
297 { |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
298 p = fd->path; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
299 break; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
300 } |
442 | 301 |
444 | 302 work2 = fd->sidecar_files; |
516 | 303 while (work2) |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
304 { |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
305 FileData *sfd = work2->data; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
306 work2 = work2->next; |
442 | 307 |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
308 if (strcasecmp(ext, sfd->extension) == 0) |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
309 { |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
310 p = sfd->path; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
311 break; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
312 } |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
313 } |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
314 if (p) break; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
315 } |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
316 string_list_free(ext_list); |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
317 if (!p) return NULL; |
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
318 } |
138 | 319 } |
140 | 320 else if (type == PATH_DEST) |
138 | 321 { |
322 if (fd->change && fd->change->dest) | |
323 p = fd->change->dest; | |
324 else | |
325 p = ""; | |
326 } | |
444 | 327 |
123
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
328 while (*p != '\0') |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
329 { |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
330 /* must escape \, ", `, and $ to avoid problems, |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
331 * we assume system shell supports bash-like escaping |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
332 */ |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
333 if (strchr("\\\"`$", *p) != NULL) |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
334 { |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
335 string = g_string_append_c(string, '\\'); |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
336 } |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
337 string = g_string_append_c(string, *p); |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
338 p++; |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
339 } |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
340 |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
341 pathl = path_from_utf8(string->str); |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
342 g_string_free(string, TRUE); |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
343 |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
344 return pathl; |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
345 } |
3602a4aa7c71
Sat Dec 2 20:15:22 2006 John Ellis <johne@verizon.net>
gqview
parents:
60
diff
changeset
|
346 |
9 | 347 |
348 /* | |
349 * The supported macros for editor commands: | |
350 * | |
351 * %f first occurence replaced by quoted sequence of filenames, command is run once. | |
352 * only one occurence of this macro is supported. | |
353 * ([ls %f] results in [ls "file1" "file2" ... "lastfile"]) | |
354 * %p command is run for each filename in turn, each instance replaced with single filename. | |
355 * multiple occurences of this macro is supported for complex shell commands. | |
356 * This macro will BLOCK THE APPLICATION until it completes, since command is run once | |
357 * for every file in syncronous order. To avoid blocking add the %v macro, below. | |
358 * ([ls %p] results in [ls "file1"], [ls "file2"] ... [ls "lastfile"]) | |
359 * none if no macro is supplied, the result is equivalent to "command %f" | |
360 * ([ls] results in [ls "file1" "file2" ... "lastfile"]) | |
361 * | |
362 * Only one of the macros %f or %p may be used in a given commmand. | |
363 * | |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
364 * %v must be the first two characters[1] in a command, causes a window to display |
9 | 365 * showing the output of the command(s). |
366 * %V same as %v except in the case of %p only displays a window for multiple files, | |
367 * operating on a single file is suppresses the output dialog. | |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
368 * |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
369 * %w must be first two characters in a command, presence will disable full screen |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
370 * from exiting upon invocation. |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
371 * |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
372 * |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
373 * [1] Note: %v,%V may also be preceded by "%w". |
9 | 374 */ |
140 | 375 |
376 | |
377 gint editor_command_parse(const gchar *template, GList *list, gchar **output) | |
9 | 378 { |
140 | 379 gint flags = 0; |
380 const gchar *p = template; | |
381 GString *result = NULL; | |
382 gchar *extensions = NULL; | |
442 | 383 |
140 | 384 if (output) |
385 result = g_string_new(""); | |
386 | |
442 | 387 if (!template || template[0] == '\0') |
140 | 388 { |
389 flags |= EDITOR_ERROR_EMPTY; | |
390 goto err; | |
391 } | |
669 | 392 |
393 /* skip leading whitespaces if any */ | |
394 while (g_ascii_isspace(*p)) p++; | |
442 | 395 |
140 | 396 /* global flags */ |
397 while (*p == '%') | |
398 { | |
399 switch (*++p) | |
400 { | |
401 case 'w': | |
402 flags |= EDITOR_KEEP_FS; | |
403 p++; | |
404 break; | |
405 case 'v': | |
406 flags |= EDITOR_VERBOSE; | |
407 p++; | |
408 break; | |
409 case 'V': | |
410 flags |= EDITOR_VERBOSE_MULTI; | |
411 p++; | |
412 break; | |
669 | 413 default: |
414 flags |= EDITOR_ERROR_SYNTAX; | |
415 goto err; | |
140 | 416 } |
417 } | |
442 | 418 |
669 | 419 /* skip whitespaces if any */ |
420 while (g_ascii_isspace(*p)) p++; | |
421 | |
140 | 422 /* command */ |
442 | 423 |
140 | 424 while (*p) |
425 { | |
426 if (*p != '%') | |
427 { | |
428 if (output) result = g_string_append_c(result, *p); | |
429 } | |
430 else /* *p == '%' */ | |
431 { | |
432 extensions = NULL; | |
433 gchar *pathl = NULL; | |
9 | 434 |
140 | 435 p++; |
442 | 436 |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
437 /* for example "%f" or "%{.crw;.raw;.cr2}f" */ |
140 | 438 if (*p == '{') |
439 { | |
444 | 440 gchar *end; |
441 | |
140 | 442 p++; |
444 | 443 end = strchr(p, '}'); |
140 | 444 if (!end) |
445 { | |
446 flags |= EDITOR_ERROR_SYNTAX; | |
447 goto err; | |
448 } | |
442 | 449 |
140 | 450 extensions = g_strndup(p, end - p); |
451 p = end + 1; | |
452 } | |
442 | 453 |
454 switch (*p) | |
140 | 455 { |
456 case 'd': | |
457 flags |= EDITOR_DEST; | |
669 | 458 /* fall through */ |
140 | 459 case 'p': |
460 flags |= EDITOR_FOR_EACH; | |
461 if (flags & EDITOR_SINGLE_COMMAND) | |
462 { | |
463 flags |= EDITOR_ERROR_INCOMPATIBLE; | |
464 goto err; | |
465 } | |
466 if (output) | |
467 { | |
468 /* use the first file from the list */ | |
442 | 469 if (!list || !list->data) |
140 | 470 { |
471 flags |= EDITOR_ERROR_NO_FILE; | |
472 goto err; | |
473 } | |
669 | 474 pathl = editor_command_path_parse((FileData *)list->data, |
475 (flags & EDITOR_DEST) ? PATH_DEST : PATH_FILE, | |
476 extensions); | |
442 | 477 if (!pathl) |
140 | 478 { |
479 flags |= EDITOR_ERROR_NO_FILE; | |
480 goto err; | |
481 } | |
482 result = g_string_append_c(result, '"'); | |
483 result = g_string_append(result, pathl); | |
484 g_free(pathl); | |
485 result = g_string_append_c(result, '"'); | |
486 } | |
442 | 487 break; |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
488 |
140 | 489 case 'f': |
490 flags |= EDITOR_SINGLE_COMMAND; | |
491 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST)) | |
492 { | |
493 flags |= EDITOR_ERROR_INCOMPATIBLE; | |
494 goto err; | |
495 } | |
496 | |
497 if (output) | |
498 { | |
499 /* use whole list */ | |
500 GList *work = list; | |
501 gboolean ok = FALSE; | |
444 | 502 |
140 | 503 while (work) |
504 { | |
505 FileData *fd = work->data; | |
506 pathl = editor_command_path_parse(fd, PATH_FILE, extensions); | |
507 | |
508 if (pathl) | |
509 { | |
510 ok = TRUE; | |
511 if (work != list) g_string_append_c(result, ' '); | |
512 result = g_string_append_c(result, '"'); | |
513 result = g_string_append(result, pathl); | |
514 g_free(pathl); | |
515 result = g_string_append_c(result, '"'); | |
516 } | |
517 work = work->next; | |
518 } | |
442 | 519 if (!ok) |
140 | 520 { |
521 flags |= EDITOR_ERROR_NO_FILE; | |
522 goto err; | |
523 } | |
524 } | |
442 | 525 break; |
669 | 526 case '%': |
527 /* %% = % escaping */ | |
528 if (output) result = g_string_append_c(result, *p); | |
529 break; | |
140 | 530 default: |
531 flags |= EDITOR_ERROR_SYNTAX; | |
532 goto err; | |
533 } | |
534 if (extensions) g_free(extensions); | |
535 extensions = NULL; | |
536 } | |
537 p++; | |
9 | 538 } |
539 | |
140 | 540 if (output) *output = g_string_free(result, FALSE); |
541 return flags; | |
542 | |
442 | 543 |
140 | 544 err: |
442 | 545 if (output) |
9 | 546 { |
140 | 547 g_string_free(result, TRUE); |
548 *output = NULL; | |
549 } | |
550 if (extensions) g_free(extensions); | |
551 return flags; | |
552 } | |
553 | |
554 static void editor_child_exit_cb (GPid pid, gint status, gpointer data) | |
555 { | |
556 EditorData *ed = data; | |
557 g_spawn_close_pid(pid); | |
558 ed->pid = -1; | |
442 | 559 |
140 | 560 editor_command_next_finish(ed, status); |
561 } | |
562 | |
563 | |
564 static gint editor_command_one(const gchar *template, GList *list, EditorData *ed) | |
565 { | |
566 gchar *command; | |
567 FileData *fd = list->data; | |
568 GPid pid; | |
442 | 569 gint standard_output; |
570 gint standard_error; | |
140 | 571 gboolean ok; |
572 | |
573 ed->pid = -1; | |
574 ed->flags = editor_command_parse(template, list, &command); | |
575 | |
576 ok = !(ed->flags & EDITOR_ERROR_MASK); | |
577 | |
578 if (ok) | |
579 { | |
737
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
580 ok = (options->shell.path && *options->shell.path); |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
581 if (!ok) log_printf("ERROR: empty shell command\n"); |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
582 |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
583 if (ok) |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
584 { |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
585 ok = (access(options->shell.path, X_OK) == 0); |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
586 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path); |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
587 } |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
588 |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
589 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC; |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
590 } |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
591 |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
592 if (ok) |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
593 { |
443 | 594 gchar *working_directory; |
595 gchar *args[4]; | |
737
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
596 guint n = 0; |
443 | 597 |
598 working_directory = remove_level_from_path(fd->path); | |
737
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
599 args[n++] = options->shell.path; |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
600 if (options->shell.options && *options->shell.options) |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
601 args[n++] = options->shell.options; |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
602 args[n++] = command; |
8a8873e7a552
Make shell command and its option rc file options instead of hardcoded strings.
zas_
parents:
731
diff
changeset
|
603 args[n] = NULL; |
443 | 604 |
442 | 605 ok = g_spawn_async_with_pipes(working_directory, args, NULL, |
140 | 606 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */ |
442 | 607 NULL, NULL, |
608 &pid, | |
609 NULL, | |
610 ed->vd ? &standard_output : NULL, | |
611 ed->vd ? &standard_error : NULL, | |
140 | 612 NULL); |
443 | 613 |
614 g_free(working_directory); | |
442 | 615 |
140 | 616 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC; |
617 } | |
618 | |
442 | 619 if (ok) |
140 | 620 { |
621 g_child_watch_add(pid, editor_child_exit_cb, ed); | |
622 ed->pid = pid; | |
623 } | |
442 | 624 |
140 | 625 if (ed->vd) |
626 { | |
627 if (!ok) | |
9 | 628 { |
140 | 629 gchar *buf; |
630 | |
147
b2266996fa83
added possibility to specify prefered file type for external commands
nadvornik
parents:
140
diff
changeset
|
631 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), template); |
140 | 632 editor_verbose_window_fill(ed->vd, buf, strlen(buf)); |
633 g_free(buf); | |
634 | |
635 } | |
442 | 636 else |
140 | 637 { |
638 GIOChannel *channel_output; | |
639 GIOChannel *channel_error; | |
444 | 640 |
140 | 641 channel_output = g_io_channel_unix_new(standard_output); |
642 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL); | |
643 | |
644 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP, | |
645 editor_verbose_io_cb, ed, NULL); | |
646 g_io_channel_unref(channel_output); | |
647 | |
648 channel_error = g_io_channel_unix_new(standard_error); | |
649 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL); | |
650 | |
651 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP, | |
652 editor_verbose_io_cb, ed, NULL); | |
653 g_io_channel_unref(channel_error); | |
654 } | |
655 } | |
442 | 656 |
140 | 657 g_free(command); |
658 | |
659 return ed->flags & EDITOR_ERROR_MASK; | |
660 } | |
661 | |
662 static gint editor_command_next_start(EditorData *ed) | |
663 { | |
664 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1); | |
665 | |
666 if (ed->list && ed->count < ed->total) | |
667 { | |
668 FileData *fd; | |
669 gint error; | |
670 | |
671 fd = ed->list->data; | |
672 | |
673 if (ed->vd) | |
674 { | |
675 editor_verbose_window_progress(ed, (ed->flags & EDITOR_FOR_EACH) ? fd->path : _("running...")); | |
676 } | |
677 ed->count++; | |
678 | |
679 error = editor_command_one(ed->command_template, ed->list, ed); | |
680 if (!error && ed->vd) | |
681 { | |
682 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) ); | |
683 if (ed->flags & EDITOR_FOR_EACH) | |
9 | 684 { |
140 | 685 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path)); |
686 editor_verbose_window_fill(ed->vd, "\n", 1); | |
9 | 687 } |
688 } | |
140 | 689 |
442 | 690 if (!error) |
140 | 691 return 0; |
692 else | |
693 /* command was not started, call the finish immediately */ | |
694 return editor_command_next_finish(ed, 0); | |
695 } | |
442 | 696 |
140 | 697 /* everything is done */ |
237
404629011caa
Add missing return at the end of editor_command_next_start().
zas_
parents:
196
diff
changeset
|
698 return editor_command_done(ed); |
140 | 699 } |
700 | |
701 static gint editor_command_next_finish(EditorData *ed, gint status) | |
702 { | |
703 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE; | |
704 | |
705 if (status) | |
706 ed->flags |= EDITOR_ERROR_STATUS; | |
707 | |
708 if (ed->flags & EDITOR_FOR_EACH) | |
709 { | |
710 /* handle the first element from the list */ | |
711 GList *fd_element = ed->list; | |
444 | 712 |
140 | 713 ed->list = g_list_remove_link(ed->list, fd_element); |
714 if (ed->callback) | |
715 cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data); | |
716 filelist_free(fd_element); | |
9 | 717 } |
718 else | |
719 { | |
140 | 720 /* handle whole list */ |
721 if (ed->callback) | |
722 cont = ed->callback(NULL, ed->flags, ed->list, ed->data); | |
723 filelist_free(ed->list); | |
724 ed->list = NULL; | |
725 } | |
9 | 726 |
140 | 727 if (cont == EDITOR_CB_SUSPEND) |
728 return ed->flags & EDITOR_ERROR_MASK; | |
729 else if (cont == EDITOR_CB_SKIP) | |
730 return editor_command_done(ed); | |
731 else | |
732 return editor_command_next_start(ed); | |
733 } | |
9 | 734 |
140 | 735 static gint editor_command_done(EditorData *ed) |
736 { | |
737 gint flags; | |
9 | 738 |
140 | 739 if (ed->vd) |
740 { | |
444 | 741 const gchar *text; |
742 | |
140 | 743 if (ed->count == ed->total) |
9 | 744 { |
140 | 745 text = _("done"); |
9 | 746 } |
747 else | |
748 { | |
140 | 749 text = _("stopped by user"); |
9 | 750 } |
140 | 751 editor_verbose_window_progress(ed, text); |
752 editor_verbose_window_enable_close(ed->vd); | |
753 } | |
754 | |
755 /* free the not-handled items */ | |
756 if (ed->list) | |
757 { | |
758 ed->flags |= EDITOR_ERROR_SKIPPED; | |
759 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data); | |
760 filelist_free(ed->list); | |
761 ed->list = NULL; | |
762 } | |
9 | 763 |
140 | 764 ed->count = 0; |
765 | |
766 flags = ed->flags & EDITOR_ERROR_MASK; | |
767 | |
768 if (!ed->vd) editor_data_free(ed); | |
769 | |
770 return flags; | |
771 } | |
772 | |
773 void editor_resume(gpointer ed) | |
774 { | |
775 editor_command_next_start(ed); | |
776 } | |
443 | 777 |
140 | 778 void editor_skip(gpointer ed) |
779 { | |
442 | 780 editor_command_done(ed); |
9 | 781 } |
782 | |
140 | 783 static gint editor_command_start(const gchar *template, const gchar *text, GList *list, EditorCallback cb, gpointer data) |
784 { | |
785 EditorData *ed; | |
786 gint flags = editor_command_parse(template, NULL, NULL); | |
442 | 787 |
140 | 788 if (flags & EDITOR_ERROR_MASK) return flags & EDITOR_ERROR_MASK; |
789 | |
790 ed = g_new0(EditorData, 1); | |
791 ed->list = filelist_copy(list); | |
792 ed->flags = flags; | |
793 ed->command_template = g_strdup(template); | |
794 ed->total = (flags & EDITOR_SINGLE_COMMAND) ? 1 : g_list_length(list); | |
795 ed->count = 0; | |
796 ed->stopping = FALSE; | |
797 ed->callback = cb; | |
798 ed->data = data; | |
442 | 799 |
140 | 800 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next) |
801 flags |= EDITOR_VERBOSE; | |
442 | 802 |
140 | 803 if (flags & EDITOR_VERBOSE) |
804 editor_verbose_window(ed, text); | |
442 | 805 |
806 editor_command_next_start(ed); | |
140 | 807 /* errors from editor_command_next_start will be handled via callback */ |
808 return flags & EDITOR_ERROR_MASK; | |
809 } | |
810 | |
444 | 811 static gint is_valid_editor_command(gint n) |
812 { | |
813 return (n >= 0 && n < GQ_EDITOR_SLOTS | |
730 | 814 && options->editor[n].command |
815 && strlen(options->editor[n].command) > 0); | |
444 | 816 } |
817 | |
140 | 818 gint start_editor_from_filelist_full(gint n, GList *list, EditorCallback cb, gpointer data) |
9 | 819 { |
820 gchar *command; | |
140 | 821 gint error; |
9 | 822 |
444 | 823 if (!list) return FALSE; |
824 if (!is_valid_editor_command(n)) return FALSE; | |
9 | 825 |
730 | 826 command = g_locale_from_utf8(options->editor[n].command, -1, NULL, NULL, NULL); |
827 error = editor_command_start(command, options->editor[n].name, list, cb, data); | |
9 | 828 g_free(command); |
669 | 829 |
670 | 830 if (n < GQ_EDITOR_GENERIC_SLOTS && (error & EDITOR_ERROR_MASK)) |
669 | 831 { |
670 | 832 gchar *text = g_strdup_printf(_("%s\n#%d \"%s\":\n%s"), editor_get_error_str(error), n+1, |
730 | 833 options->editor[n].name, options->editor[n].command); |
669 | 834 |
835 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL); | |
836 g_free(text); | |
837 } | |
838 | |
140 | 839 return error; |
9 | 840 } |
841 | |
140 | 842 gint start_editor_from_filelist(gint n, GList *list) |
843 { | |
844 return start_editor_from_filelist_full(n, list, NULL, NULL); | |
845 } | |
846 | |
847 gint start_editor_from_file_full(gint n, FileData *fd, EditorCallback cb, gpointer data) | |
9 | 848 { |
849 GList *list; | |
140 | 850 gint error; |
9 | 851 |
138 | 852 if (!fd) return FALSE; |
9 | 853 |
138 | 854 list = g_list_append(NULL, fd); |
140 | 855 error = start_editor_from_filelist_full(n, list, cb, data); |
9 | 856 g_list_free(list); |
140 | 857 return error; |
9 | 858 } |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
859 |
140 | 860 gint start_editor_from_file(gint n, FileData *fd) |
136 | 861 { |
140 | 862 return start_editor_from_file_full(n, fd, NULL, NULL); |
136 | 863 } |
864 | |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
865 gint editor_window_flag_set(gint n) |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
866 { |
444 | 867 if (!is_valid_editor_command(n)) return TRUE; |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
868 |
730 | 869 return (editor_command_parse(options->editor[n].command, NULL, NULL) & EDITOR_KEEP_FS); |
60
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
870 } |
9c0c402b0ef3
Mon Jun 13 17:31:46 2005 John Ellis <johne@verizon.net>
gqview
parents:
9
diff
changeset
|
871 |
140 | 872 const gchar *editor_get_error_str(gint flags) |
873 { | |
874 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty."); | |
875 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax."); | |
876 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros."); | |
877 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type."); | |
878 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor."); | |
879 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status."); | |
880 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped."); | |
881 return _("Unknown error."); | |
882 } | |
731
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
883 |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
884 const gchar *editor_get_name(gint n) |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
885 { |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
886 if (!is_valid_editor_command(n)) return NULL; |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
887 |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
888 if (options->editor[n].name && strlen(options->editor[n].name) > 0) |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
889 return options->editor[n].name; |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
890 |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
891 return _("(unknown)"); |
fa8f7d7396cf
Introduce an helper function that returns the name of an editor.
zas_
parents:
730
diff
changeset
|
892 } |