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