more external commands
[geeqie.git] / src / editors.c
1 /*
2  * GQview
3  * (C) 2006 John Ellis
4  *
5  * Author: John Ellis
6  *
7  * This software is released under the GNU General Public License (GNU GPL).
8  * Please read the included file COPYING for more information.
9  * This software comes with no warranty of any kind, use at your own risk!
10  */
11
12
13 #include "gqview.h"
14 #include "editors.h"
15
16 #include "utilops.h"
17 #include "ui_fileops.h"
18 #include "ui_spinner.h"
19 #include "ui_utildlg.h"
20
21 #include <errno.h>
22
23
24 #define EDITOR_WINDOW_WIDTH 500
25 #define EDITOR_WINDOW_HEIGHT 300
26
27 #define COMMAND_SHELL "sh"
28 #define COMMAND_OPT  "-c"
29
30
31 typedef struct _EditorVerboseData EditorVerboseData;
32 struct _EditorVerboseData {
33         int fd;
34
35         GenericDialog *gd;
36         GtkWidget *button_close;
37         GtkWidget *button_stop;
38         GtkWidget *text;
39         GtkWidget *progress;
40         GtkWidget *spinner;
41         gint count;
42         gint total;
43
44         gchar *command_template;
45         GList *list;
46 };
47
48
49 static gchar *editor_slot_defaults[GQVIEW_EDITOR_SLOTS * 2] = {
50         N_("The Gimp"), "gimp-remote -n %f",
51         N_("XV"), "xv %f",
52         N_("Xpaint"), "xpaint %f",
53         NULL, NULL,
54         NULL, NULL,
55         NULL, NULL,
56         NULL, NULL,
57         NULL, NULL,
58         N_("Rotate jpeg clockwise"), "%vif jpegtran -rotate 90 -copy all -outfile %p_tmp %p; then mv %p_tmp %p;else rm %p_tmp;fi",
59         N_("Rotate jpeg counterclockwise"), "%vif jpegtran -rotate 270 -copy all -outfile %p_tmp %p; then mv %p_tmp %p;else rm %p_tmp;fi",
60         /* special slots */
61 #if 1
62         /* for testing */
63         "External Copy command", "%vset -x;cp %f",
64         "External Move command", "%vset -x;mv %f",
65         "External Rename command", "%vset -x;mv %f",
66         "External Delete command", "%vset -x;rm %f",
67         "External New Folder command", NULL
68 #else
69         "External Copy command", NULL,
70         "External Move command", NULL,
71         "External Rename command", NULL,
72         "External Delete command", NULL,
73         "External New Folder command", NULL
74 #endif
75 };
76
77 static void editor_verbose_window_progress(EditorVerboseData *vd, const gchar *text);
78 static gint editor_command_next(EditorVerboseData *vd);
79
80
81 /*
82  *-----------------------------------------------------------------------------
83  * external editor routines
84  *-----------------------------------------------------------------------------
85  */
86
87 void editor_reset_defaults(void)
88 {
89         gint i;
90
91         for (i = 0; i < GQVIEW_EDITOR_SLOTS; i++)
92                 {
93                 g_free(editor_name[i]);
94                 editor_name[i] = g_strdup(_(editor_slot_defaults[i * 2]));
95                 g_free(editor_command[i]);
96                 editor_command[i] = g_strdup(editor_slot_defaults[i * 2 + 1]);
97                 }
98 }
99
100 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
101 {
102         EditorVerboseData *vd = data;
103
104         generic_dialog_close(gd);
105         g_free(vd->command_template);
106         g_free(vd);
107 }
108
109 static void editor_verbose_window_stop(GenericDialog *gd, gpointer data)
110 {
111         EditorVerboseData *vd = data;
112
113         path_list_free(vd->list);
114         vd->list = NULL;
115
116         vd->count = 0;
117         editor_verbose_window_progress(vd, _("stopping..."));
118 }
119
120 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
121 {
122         vd->gd->cancel_cb = editor_verbose_window_close;
123
124         spinner_set_interval(vd->spinner, -1);
125         gtk_widget_set_sensitive(vd->button_stop, FALSE);
126         gtk_widget_set_sensitive(vd->button_close, TRUE);
127 }
128
129 static EditorVerboseData *editor_verbose_window(const gchar *template, const gchar *text)
130 {
131         EditorVerboseData *vd;
132         GtkWidget *scrolled;
133         GtkWidget *hbox;
134         gchar *buf;
135
136         vd = g_new0(EditorVerboseData, 1);
137
138         vd->list = NULL;
139         vd->command_template = g_strdup(template);
140         vd->total = 0;
141         vd->count = 0;
142         vd->fd = -1;
143
144         vd->gd = file_util_gen_dlg(_("Edit command results"), "GQview", "editor_results",
145                                    NULL, FALSE,
146                                    NULL, vd);
147         buf = g_strdup_printf(_("Output of %s"), text);
148         generic_dialog_add_message(vd->gd, NULL, buf, NULL);
149         g_free(buf);
150         vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
151                                                    editor_verbose_window_stop, FALSE);
152         gtk_widget_set_sensitive(vd->button_stop, FALSE);
153         vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
154                                                     editor_verbose_window_close, TRUE);
155         gtk_widget_set_sensitive(vd->button_close, FALSE);
156
157         scrolled = gtk_scrolled_window_new(NULL, NULL);
158         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
159         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
160                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
161         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
162         gtk_widget_show(scrolled);
163
164         vd->text = gtk_text_view_new();
165         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
166         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
167         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
168         gtk_widget_show(vd->text);
169
170         hbox = gtk_hbox_new(FALSE, 0);
171         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
172         gtk_widget_show(hbox);
173
174         vd->progress = gtk_progress_bar_new();
175         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
176         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
177         gtk_widget_show(vd->progress);
178
179         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
180         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
181         gtk_widget_show(vd->spinner);
182         
183         gtk_widget_show(vd->gd->dialog);
184
185         return vd;
186 }
187
188 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
189 {
190         GtkTextBuffer *buffer;
191         GtkTextIter iter;
192
193         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
194         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
195         gtk_text_buffer_insert(buffer, &iter, text, len);
196 }
197
198 static void editor_verbose_window_progress(EditorVerboseData *vd, const gchar *text)
199 {
200         if (vd->total)
201                 {
202                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), (double)vd->count / vd->total);
203                 }
204
205         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), (text) ? text : "");
206 }
207
208 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
209 {
210         EditorVerboseData *vd = data;
211         gchar buf[512];
212         gsize count;
213
214         switch (condition)
215                 {
216                 case G_IO_IN:
217                         while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
218                                 {
219                                 if (!g_utf8_validate(buf, count, NULL))
220                                         {
221                                         gchar *utf8;
222                                         utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
223                                         if (utf8)
224                                                 {
225                                                 editor_verbose_window_fill(vd, utf8, -1);
226                                                 g_free(utf8);
227                                                 }
228                                         else
229                                                 {
230                                                 editor_verbose_window_fill(vd, "GQview: Error converting text to valid utf8\n", -1);
231                                                 }
232                                         }
233                                 else
234                                         {
235                                         editor_verbose_window_fill(vd, buf, count);
236                                         }
237                                 }
238                         break;
239                 case G_IO_ERR:
240                         printf("Error reading from command\n");
241                 case G_IO_HUP:
242                         if (debug) printf("Editor command HUP\n");
243                 default:
244                         while (g_source_remove_by_user_data(vd));
245                         close(vd->fd);
246                         vd->fd = -1;
247                         editor_command_next(vd);
248                         return FALSE;
249                         break;
250                 }
251
252         return TRUE;
253 }
254
255 static int command_pipe(char *command)
256 {
257         char *args[4];
258         int fpipe[2];
259         pid_t fpid;
260
261         args[0] = COMMAND_SHELL;
262         args[1] = COMMAND_OPT;
263         args[2] = command;
264         args[3] = NULL;
265
266         if (pipe(fpipe) < 0)
267                 {
268                 printf("pipe setup failed: %s\n", strerror(errno));
269                 return -1;
270                 }
271
272         fpid = fork();
273         if (fpid < 0)
274                 {
275                 /* fork failed */
276                 printf("fork failed: %s\n", strerror(errno));
277                 }
278         else if (fpid == 0)
279                 {
280                 /* child */
281                 gchar *msg;
282
283                 dup2(fpipe[1], 1);
284                 dup2(fpipe[1], 2);
285                 close(fpipe[0]);
286
287                 execvp(args[0], args);
288
289                 msg = g_strdup_printf("Unable to exec command:\n%s\n\n%s\n", command, strerror(errno));
290                 write(1, msg, strlen(msg));
291
292                 _exit(1);
293                 }
294         else
295                 {
296                 /* parent */
297                 fcntl(fpipe[0], F_SETFL, O_NONBLOCK);
298                 close(fpipe[1]);
299
300                 return fpipe[0];
301                 }
302
303         return -1;
304 }
305
306 static gint editor_verbose_start(EditorVerboseData *vd, gchar *command)
307         {
308         GIOChannel *channel;
309         int fd;
310
311         fd = command_pipe(command);
312         if (fd < 0)
313                 {
314                 gchar *buf;
315
316                 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), command);
317                 editor_verbose_window_fill(vd, buf, strlen(buf));
318                 g_free(buf);
319
320                 return FALSE;
321                 }
322
323         vd->fd = fd;
324         channel = g_io_channel_unix_new(fd);
325
326         g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, G_IO_IN,
327                             editor_verbose_io_cb, vd, NULL);
328         g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, G_IO_ERR,
329                             editor_verbose_io_cb, vd, NULL);
330         g_io_add_watch_full(channel, G_PRIORITY_DEFAULT, G_IO_HUP,
331                             editor_verbose_io_cb, vd, NULL);
332         g_io_channel_unref(channel);
333
334         return TRUE;
335 }
336
337 static gchar *editor_command_path_parse(const gchar *path)
338 {
339         GString *string;
340         gchar *pathl;
341         const gchar *p;
342
343         string = g_string_new("");
344         p = path;
345         while (*p != '\0')
346                 {
347                 /* must escape \, ", `, and $ to avoid problems,
348                  * we assume system shell supports bash-like escaping
349                  */
350                 if (strchr("\\\"`$", *p) != NULL)
351                         {
352                         string = g_string_append_c(string, '\\');
353                         }
354                 string = g_string_append_c(string, *p);
355                 p++;
356                 }
357
358         pathl = path_from_utf8(string->str);
359         g_string_free(string, TRUE);
360
361         return pathl;
362 }
363
364 static gint editor_command_one(const gchar *template, const gchar *path, EditorVerboseData *vd)
365 {
366         GString *result = NULL;
367         gchar *pathl;
368         gchar *found;
369         const gchar *ptr;
370         gchar path_buffer[512];
371         gchar *current_path;
372         gint path_change = FALSE;
373         gint ret;
374
375         current_path = getcwd(path_buffer, sizeof(path_buffer));
376
377         result = g_string_new("");
378         pathl = editor_command_path_parse(path);
379
380         ptr = template;
381         while ( (found = strstr(ptr, "%p")) )
382                 {
383                 result = g_string_append_len(result, ptr, found - ptr);
384                 ptr = found + 2;
385                 result = g_string_append_c(result, '"');
386                 result = g_string_append(result, pathl);
387                 result = g_string_append_c(result, '"');
388                 }
389         result = g_string_append(result, ptr);
390
391         if (debug) printf("system command: %s\n", result->str);
392
393         if (current_path)
394                 {
395                 gchar *base;
396                 base = remove_level_from_path(path);
397                 if (chdir(base) == 0) path_change = TRUE;
398                 g_free(base);
399                 }
400
401         if (vd)
402                 {
403                 result = g_string_append(result, " 2>&1");
404                 ret = editor_verbose_start(vd, result->str);
405                 }
406         else
407                 {
408                 ret = !system(result->str);
409                 }
410
411         if (path_change) chdir(current_path);
412
413         g_string_free(result, TRUE);
414         g_free(pathl);
415
416         return ret;
417 }
418
419 static gint editor_command_next(EditorVerboseData *vd)
420 {
421         const gchar *text;
422
423         editor_verbose_window_fill(vd, "\n", 1);
424
425         while (vd->list)
426                 {
427                 gchar *path;
428                 gint success;
429
430                 path = vd->list->data;
431                 vd->list = g_list_remove(vd->list, path);
432
433                 editor_verbose_window_progress(vd, path);
434
435                 vd->count++;
436                 success = editor_command_one(vd->command_template, path, vd);
437                 if (success)
438                         {
439                         gtk_widget_set_sensitive(vd->button_stop, (vd->list != NULL) );
440                         editor_verbose_window_fill(vd, path, strlen(path));
441                         editor_verbose_window_fill(vd, "\n", 1);
442                         }
443
444                 g_free(path);
445                 if (success) return TRUE;
446                 }
447
448         if (vd->count == vd->total)
449                 {
450                 text = _("done");
451                 }
452         else
453                 {
454                 text = _("stopped by user");
455                 }
456         vd->count = 0;
457         editor_verbose_window_progress(vd, text);
458         editor_verbose_window_enable_close(vd);
459         return FALSE;
460 }
461
462 static gint editor_command_start(const gchar *template, const gchar *text, GList *list)
463 {
464         EditorVerboseData *vd;
465
466         vd = editor_verbose_window(template, text);
467         vd->list = path_list_copy(list);
468         vd->total = g_list_length(list);
469
470         return editor_command_next(vd);
471 }
472
473 static gint editor_line_break(const gchar *template, gchar **front, const gchar **end)
474 {
475         gchar *found;
476
477         *front = g_strdup(template);
478         found = strstr(*front, "%f");
479
480         if (found)
481                 {
482                 *found = '\0';
483                 *end = found + 2;
484                 return TRUE;
485                 }
486
487         *end = "";
488         return FALSE;
489 }
490
491 /*
492  * The supported macros for editor commands:
493  *
494  *   %f   first occurence replaced by quoted sequence of filenames, command is run once.
495  *        only one occurence of this macro is supported.
496  *        ([ls %f] results in [ls "file1" "file2" ... "lastfile"])
497  *   %p   command is run for each filename in turn, each instance replaced with single filename.
498  *        multiple occurences of this macro is supported for complex shell commands.
499  *        This macro will BLOCK THE APPLICATION until it completes, since command is run once
500  *        for every file in syncronous order. To avoid blocking add the %v macro, below.
501  *        ([ls %p] results in [ls "file1"], [ls "file2"] ... [ls "lastfile"])
502  *   none if no macro is supplied, the result is equivalent to "command %f"
503  *        ([ls] results in [ls "file1" "file2" ... "lastfile"])
504  *
505  *  Only one of the macros %f or %p may be used in a given commmand.
506  *
507  *   %v   must be the first two characters[1] in a command, causes a window to display
508  *        showing the output of the command(s).
509  *   %V   same as %v except in the case of %p only displays a window for multiple files,
510  *        operating on a single file is suppresses the output dialog.
511  *
512  *   %w   must be first two characters in a command, presence will disable full screen
513  *        from exiting upon invocation.
514  *
515  *
516  * [1] Note: %v,%V may also be preceded by "%w".
517  */
518 static gint editor_command_run(const gchar *template, const gchar *text, GList *list)
519 {
520         gint verbose = FALSE;
521         gint for_each = FALSE;
522         gint ret = TRUE;
523
524         if (!template || template[0] == '\0') return;
525
526         for_each = (strstr(template, "%p") != NULL);
527
528         /* no window state change flag, skip */
529         if (strncmp(template, "%w", 2) == 0) template += 2;
530
531         if (strncmp(template, "%v", 2) == 0)
532                 {
533                 template += 2;
534                 verbose = TRUE;
535                 }
536         else if (strncmp(template, "%V", 2) == 0)
537                 {
538                 template += 2;
539                 if (!for_each || list->next) verbose = TRUE;
540                 }
541
542         if (for_each)
543                 {
544                 if (verbose)
545                         {
546                         editor_command_start(template, text, list);
547                         }
548                 else
549                         {
550                         GList *work;
551
552                         work = list;
553                         while (work)
554                                 {
555                                 gchar *path = work->data;
556                                 ret = editor_command_one(template, path, NULL);
557                                 work = work->next;
558                                 }
559                         }
560                 }
561         else
562                 {
563                 gchar *front;
564                 const gchar *end;
565                 GList *work;
566                 GString *result = NULL;
567                 gint parser_match;
568
569                 parser_match = editor_line_break(template, &front, &end);
570                 result = g_string_new((parser_match) ? "" : " ");
571
572                 work = list;
573                 while (work)
574                         {
575                         gchar *path = work->data;
576                         gchar *pathl;
577
578                         if (work != list) g_string_append_c(result, ' ');
579                         result = g_string_append_c(result, '"');
580                         pathl = editor_command_path_parse(path);
581                         result = g_string_append(result, pathl);
582                         g_free(pathl);
583                         result = g_string_append_c(result, '"');
584                         work = work->next;
585                         }
586
587                 result = g_string_prepend(result, front);
588                 result = g_string_append(result, end);
589                 if (verbose) result = g_string_append(result, " 2>&1 ");
590                 result = g_string_append(result, "&");
591
592                 if (debug) printf("system command: %s\n", result->str);
593
594                 if (verbose)
595                         {
596                         EditorVerboseData *vd;
597
598                         vd = editor_verbose_window(template, text);
599                         editor_verbose_window_progress(vd, _("running..."));
600                         ret = editor_verbose_start(vd, result->str);
601                         }
602                 else
603                         {
604                         ret = !system(result->str);
605                         }
606
607                 g_free(front);
608                 g_string_free(result, TRUE);
609                 }
610         return ret;
611 }
612
613 gint start_editor_from_path_list(gint n, GList *list)
614 {
615         gchar *command;
616         gint ret;
617
618         if (n < 0 || n >= GQVIEW_EDITOR_SLOTS || !list ||
619             !editor_command[n] ||
620             strlen(editor_command[n]) == 0) return FALSE;
621
622         command = g_locale_from_utf8(editor_command[n], -1, NULL, NULL, NULL);
623         ret = editor_command_run(command, editor_name[n], list);
624         g_free(command);
625         return ret;
626 }
627
628 gint start_editor_from_file(gint n, const gchar *path)
629 {
630         GList *list;
631         gint ret;
632
633         if (!path) return FALSE;
634
635         list = g_list_append(NULL, (gchar *)path);
636         ret = start_editor_from_path_list(n, list);
637         g_list_free(list);
638         return ret;
639 }
640
641 gint start_editor_from_pair(gint n, const gchar *source, const gchar *target)
642 {
643         GList *list;
644         gint ret;
645
646         if (!source) return FALSE;
647         if (!target) return FALSE;
648
649         list = g_list_append(NULL, (gchar *)source);
650         list = g_list_append(list, (gchar *)target);
651         ret = start_editor_from_path_list(n, list);
652         g_list_free(list);
653         return ret;
654 }
655
656 gint editor_window_flag_set(gint n)
657 {
658         if (n < 0 || n >= GQVIEW_EDITOR_SLOTS ||
659             !editor_command[n] ||
660             strlen(editor_command[n]) == 0) return TRUE;
661
662         return (strncmp(editor_command[n], "%w", 2) == 0);
663 }
664
665