replaced gchar* path with FileData *fd
[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 %p %t",
64         "External Move command", "%vset -x;mv %p %t",
65         "External Rename command", "%vset -x;mv %p %t",
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         filelist_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 typedef enum {
338         PATH_FILE,
339         PATH_TARGET
340 } PathType;
341
342
343 static gchar *editor_command_path_parse(const FileData *fd, PathType type)
344 {
345         GString *string;
346         gchar *pathl;
347         const gchar *p;
348
349         string = g_string_new("");
350         
351         if (type == PATH_FILE)
352                 {
353                 p = fd->path;
354                 }
355         else if (type == PATH_TARGET)
356                 {
357                 if (fd->change && fd->change->dest)
358                         p = fd->change->dest;
359                 else
360                         p = "";
361                 }
362         while (*p != '\0')
363                 {
364                 /* must escape \, ", `, and $ to avoid problems,
365                  * we assume system shell supports bash-like escaping
366                  */
367                 if (strchr("\\\"`$", *p) != NULL)
368                         {
369                         string = g_string_append_c(string, '\\');
370                         }
371                 string = g_string_append_c(string, *p);
372                 p++;
373                 }
374
375         pathl = path_from_utf8(string->str);
376         g_string_free(string, TRUE);
377
378         return pathl;
379 }
380
381 static gint editor_command_one(const gchar *template, const FileData *fd, EditorVerboseData *vd)
382 {
383         GString *result = NULL;
384         gchar *pathl, *targetl;
385         gchar *found;
386         const gchar *ptr;
387         gchar path_buffer[512];
388         gchar *current_path;
389         gint path_change = FALSE;
390         gint ret;
391
392         current_path = getcwd(path_buffer, sizeof(path_buffer));
393
394         result = g_string_new("");
395         pathl = editor_command_path_parse(fd, PATH_FILE);
396         targetl = editor_command_path_parse(fd, PATH_TARGET);
397
398         ptr = template;
399         while ( (found = strstr(ptr, "%")) )
400                 {
401                 result = g_string_append_len(result, ptr, found - ptr);
402                 ptr = found + 2;
403                 switch (found[1])
404                         {
405                         case 'p':
406                                 result = g_string_append_c(result, '"');
407                                 result = g_string_append(result, pathl);
408                                 result = g_string_append_c(result, '"');
409                                 break;
410                         case 't':
411                                 result = g_string_append_c(result, '"');
412                                 result = g_string_append(result, targetl);
413                                 result = g_string_append_c(result, '"');
414                                 break;
415                         case '%':
416                                 result = g_string_append_c(result, '%');
417                                 break;
418                         default:
419                                 break;
420                         }
421                 }
422         result = g_string_append(result, ptr);
423
424         if (debug) printf("system command: %s\n", result->str);
425
426         if (current_path)
427                 {
428                 gchar *base;
429                 base = remove_level_from_path(fd->path);
430                 if (chdir(base) == 0) path_change = TRUE;
431                 g_free(base);
432                 }
433
434         if (vd)
435                 {
436                 result = g_string_append(result, " 2>&1");
437                 ret = editor_verbose_start(vd, result->str);
438                 }
439         else
440                 {
441                 ret = !system(result->str);
442                 }
443
444         if (path_change) chdir(current_path);
445
446         g_string_free(result, TRUE);
447         g_free(pathl);
448         g_free(targetl);
449
450         return ret;
451 }
452
453 static gint editor_command_next(EditorVerboseData *vd)
454 {
455         const gchar *text;
456
457         editor_verbose_window_fill(vd, "\n", 1);
458
459         while (vd->list)
460                 {
461                 FileData *fd;
462                 gint success;
463
464                 fd = vd->list->data;
465                 vd->list = g_list_remove(vd->list, fd);
466
467                 editor_verbose_window_progress(vd, fd->path);
468
469                 vd->count++;
470                 success = editor_command_one(vd->command_template, fd, vd);
471                 if (success)
472                         {
473                         gtk_widget_set_sensitive(vd->button_stop, (vd->list != NULL) );
474                         editor_verbose_window_fill(vd, fd->path, strlen(fd->path));
475                         editor_verbose_window_fill(vd, "\n", 1);
476                         }
477
478                 file_data_unref(fd);
479                 if (success) return TRUE;
480                 }
481
482         if (vd->count == vd->total)
483                 {
484                 text = _("done");
485                 }
486         else
487                 {
488                 text = _("stopped by user");
489                 }
490         vd->count = 0;
491         editor_verbose_window_progress(vd, text);
492         editor_verbose_window_enable_close(vd);
493         return FALSE;
494 }
495
496 static gint editor_command_start(const gchar *template, const gchar *text, GList *list)
497 {
498         EditorVerboseData *vd;
499
500         vd = editor_verbose_window(template, text);
501         vd->list = filelist_copy(list);
502         vd->total = g_list_length(list);
503
504         return editor_command_next(vd);
505 }
506
507 static gint editor_line_break(const gchar *template, gchar **front, const gchar **end)
508 {
509         gchar *found;
510
511         *front = g_strdup(template);
512         found = strstr(*front, "%f");
513
514         if (found)
515                 {
516                 *found = '\0';
517                 *end = found + 2;
518                 return TRUE;
519                 }
520
521         *end = "";
522         return FALSE;
523 }
524
525 /*
526  * The supported macros for editor commands:
527  *
528  *   %f   first occurence replaced by quoted sequence of filenames, command is run once.
529  *        only one occurence of this macro is supported.
530  *        ([ls %f] results in [ls "file1" "file2" ... "lastfile"])
531  *   %p   command is run for each filename in turn, each instance replaced with single filename.
532  *        multiple occurences of this macro is supported for complex shell commands.
533  *        This macro will BLOCK THE APPLICATION until it completes, since command is run once
534  *        for every file in syncronous order. To avoid blocking add the %v macro, below.
535  *        ([ls %p] results in [ls "file1"], [ls "file2"] ... [ls "lastfile"])
536  *   none if no macro is supplied, the result is equivalent to "command %f"
537  *        ([ls] results in [ls "file1" "file2" ... "lastfile"])
538  *
539  *  Only one of the macros %f or %p may be used in a given commmand.
540  *
541  *   %v   must be the first two characters[1] in a command, causes a window to display
542  *        showing the output of the command(s).
543  *   %V   same as %v except in the case of %p only displays a window for multiple files,
544  *        operating on a single file is suppresses the output dialog.
545  *
546  *   %w   must be first two characters in a command, presence will disable full screen
547  *        from exiting upon invocation.
548  *
549  *
550  * [1] Note: %v,%V may also be preceded by "%w".
551  */
552 static gint editor_command_run(const gchar *template, const gchar *text, GList *list)
553 {
554         gint verbose = FALSE;
555         gint for_each = FALSE;
556         gint ret = TRUE;
557
558         if (!template || template[0] == '\0') return;
559
560         for_each = (strstr(template, "%p") != NULL);
561
562         /* no window state change flag, skip */
563         if (strncmp(template, "%w", 2) == 0) template += 2;
564
565         if (strncmp(template, "%v", 2) == 0)
566                 {
567                 template += 2;
568                 verbose = TRUE;
569                 }
570         else if (strncmp(template, "%V", 2) == 0)
571                 {
572                 template += 2;
573                 if (!for_each || list->next) verbose = TRUE;
574                 }
575
576         if (for_each)
577                 {
578                 if (verbose)
579                         {
580                         editor_command_start(template, text, list);
581                         }
582                 else
583                         {
584                         GList *work;
585
586                         work = list;
587                         while (work)
588                                 {
589                                 FileData *fd = work->data;
590                                 ret = editor_command_one(template, fd, NULL);
591                                 work = work->next;
592                                 }
593                         }
594                 }
595         else
596                 {
597                 gchar *front;
598                 const gchar *end;
599                 GList *work;
600                 GString *result = NULL;
601                 gint parser_match;
602
603                 parser_match = editor_line_break(template, &front, &end);
604                 result = g_string_new((parser_match) ? "" : " ");
605
606                 work = list;
607                 while (work)
608                         {
609                         FileData *fd = work->data;
610                         gchar *pathl;
611
612                         if (work != list) g_string_append_c(result, ' ');
613                         result = g_string_append_c(result, '"');
614                         pathl = editor_command_path_parse(fd, PATH_FILE);
615                         result = g_string_append(result, pathl);
616                         g_free(pathl);
617                         result = g_string_append_c(result, '"');
618                         work = work->next;
619                         }
620
621                 result = g_string_prepend(result, front);
622                 result = g_string_append(result, end);
623                 if (verbose) result = g_string_append(result, " 2>&1 ");
624                 result = g_string_append(result, "&");
625
626                 if (debug) printf("system command: %s\n", result->str);
627
628                 if (verbose)
629                         {
630                         EditorVerboseData *vd;
631
632                         vd = editor_verbose_window(template, text);
633                         editor_verbose_window_progress(vd, _("running..."));
634                         ret = editor_verbose_start(vd, result->str);
635                         }
636                 else
637                         {
638                         ret = !system(result->str);
639                         }
640
641                 g_free(front);
642                 g_string_free(result, TRUE);
643                 }
644         return ret;
645 }
646
647 gint start_editor_from_filelist(gint n, GList *list)
648 {
649         gchar *command;
650         gint ret;
651
652         if (n < 0 || n >= GQVIEW_EDITOR_SLOTS || !list ||
653             !editor_command[n] ||
654             strlen(editor_command[n]) == 0) return FALSE;
655
656         command = g_locale_from_utf8(editor_command[n], -1, NULL, NULL, NULL);
657         ret = editor_command_run(command, editor_name[n], list);
658         g_free(command);
659         return ret;
660 }
661
662 gint start_editor_from_file(gint n, FileData *fd)
663 {
664         GList *list;
665         gint ret;
666
667         if (!fd) return FALSE;
668
669         list = g_list_append(NULL, fd);
670         ret = start_editor_from_filelist(n, list);
671         g_list_free(list);
672         return ret;
673 }
674
675 gint start_editor_from_pair(gint n, const gchar *source, const gchar *target)
676 {
677         GList *list;
678         gint ret;
679
680         if (!source) return FALSE;
681         if (!target) return FALSE;
682
683         list = g_list_append(NULL, (gchar *)source);
684         list = g_list_append(list, (gchar *)target);
685         ret = start_editor_from_filelist(n, list);
686         g_list_free(list);
687         return ret;
688 }
689
690 gint editor_window_flag_set(gint n)
691 {
692         if (n < 0 || n >= GQVIEW_EDITOR_SLOTS ||
693             !editor_command[n] ||
694             strlen(editor_command[n]) == 0) return TRUE;
695
696         return (strncmp(editor_command[n], "%w", 2) == 0);
697 }
698
699