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