2 * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2002 Hiroyuki Yamamoto
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 #include <gdk/gdkkeysyms.h>
34 #include <sys/types.h>
40 #include "prefs_gtk.h"
44 #include "manage_window.h"
45 #include "mainwindow.h"
46 #include "prefs_common.h"
47 #include "alertpanel.h"
48 #include "prefs_actions.h"
53 #include "description_window.h"
59 ACTION_PIPE_IN = 1 << 1,
60 ACTION_PIPE_OUT = 1 << 2,
61 ACTION_SINGLE = 1 << 3,
62 ACTION_MULTIPLE = 1 << 4,
63 ACTION_ASYNC = 1 << 5,
64 ACTION_OPEN_IN = 1 << 6,
65 ACTION_HIDE_IN = 1 << 7,
66 ACTION_INSERT = 1 << 8,
67 ACTION_ERROR = 1 << 9,
76 GtkWidget *name_entry;
79 GtkWidget *actions_clist;
82 typedef struct _Children Children;
83 typedef struct _ChildInfo ChildInfo;
90 GtkWidget *input_entry;
91 GtkWidget *input_hbox;
94 GtkWidget *scrolledwin;
123 /* widget creating functions */
124 static void prefs_actions_create (MainWindow *mainwin);
125 static void prefs_actions_set_dialog (void);
126 static gint prefs_actions_clist_set_row (gint row);
128 /* callback functions */
129 static void prefs_actions_help_cb (GtkWidget *w,
131 static void prefs_actions_register_cb (GtkWidget *w,
133 static void prefs_actions_substitute_cb (GtkWidget *w,
135 static void prefs_actions_delete_cb (GtkWidget *w,
137 static void prefs_actions_up (GtkWidget *w,
139 static void prefs_actions_down (GtkWidget *w,
141 static void prefs_actions_select (GtkCList *clist,
145 static void prefs_actions_row_move (GtkCList *clist,
148 static gint prefs_actions_deleted (GtkWidget *widget,
151 static void prefs_actions_key_pressed (GtkWidget *widget,
154 static void prefs_actions_cancel (GtkWidget *w,
156 static void prefs_actions_ok (GtkWidget *w,
158 static void update_actions_menu (GtkItemFactory *ifactory,
162 static void mainwin_actions_execute_cb (MainWindow *mainwin,
165 static void compose_actions_execute_cb (Compose *compose,
168 static void msgview_actions_execute_cb (MessageView *msgview,
171 static guint get_action_type (gchar *action);
173 static gboolean execute_actions (gchar *action,
181 static gchar *parse_action_cmd (gchar *action,
185 static gboolean parse_append_filename (GString **cmd,
188 static gboolean parse_append_msgpart (GString **cmd,
192 ChildInfo *fork_child (gchar *cmd,
199 static gint wait_for_children (gpointer data);
201 static void free_children (Children *children);
203 static void childinfo_close_pipes (ChildInfo *child_info);
205 static void create_io_dialog (Children *children);
206 static void update_io_dialog (Children *children);
208 static void hide_io_dialog_cb (GtkWidget *widget,
211 static void catch_output (gpointer data,
213 GdkInputCondition cond);
214 static void catch_input (gpointer data,
216 GdkInputCondition cond);
217 static void catch_status (gpointer data,
219 GdkInputCondition cond);
222 void prefs_actions_open(MainWindow *mainwin)
225 if (prefs_rc_is_readonly(ACTIONS_RC))
231 prefs_actions_create(mainwin);
233 manage_window_set_transient(GTK_WINDOW(actions.window));
234 gtk_widget_grab_focus(actions.ok_btn);
236 prefs_actions_set_dialog();
238 gtk_widget_show(actions.window);
241 static void prefs_actions_create(MainWindow *mainwin)
246 GtkWidget *cancel_btn;
247 GtkWidget *confirm_area;
251 GtkWidget *entry_vbox;
253 GtkWidget *name_label;
254 GtkWidget *name_entry;
255 GtkWidget *cmd_label;
256 GtkWidget *cmd_entry;
262 GtkWidget *subst_btn;
265 GtkWidget *cond_hbox;
266 GtkWidget *cond_scrolledwin;
267 GtkWidget *cond_clist;
269 GtkWidget *help_button;
277 debug_print("Creating actions configuration window...\n");
279 window = gtk_window_new (GTK_WINDOW_DIALOG);
281 gtk_container_set_border_width(GTK_CONTAINER (window), 8);
282 gtk_window_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
283 gtk_window_set_modal(GTK_WINDOW(window), TRUE);
284 gtk_window_set_policy(GTK_WINDOW(window), FALSE, TRUE, TRUE);
285 gtk_window_set_default_size(GTK_WINDOW(window), 400, -1);
287 vbox = gtk_vbox_new(FALSE, 6);
288 gtk_widget_show(vbox);
289 gtk_container_add(GTK_CONTAINER(window), vbox);
291 gtkut_button_set_create(&confirm_area, &ok_btn, _("OK"),
292 &cancel_btn, _("Cancel"), NULL, NULL);
293 gtk_widget_show(confirm_area);
294 gtk_box_pack_end(GTK_BOX(vbox), confirm_area, FALSE, FALSE, 0);
295 gtk_widget_grab_default(ok_btn);
297 gtk_window_set_title(GTK_WINDOW(window), _("Actions configuration"));
298 gtk_signal_connect(GTK_OBJECT(window), "delete_event",
299 GTK_SIGNAL_FUNC(prefs_actions_deleted), NULL);
300 gtk_signal_connect(GTK_OBJECT(window), "key_press_event",
301 GTK_SIGNAL_FUNC(prefs_actions_key_pressed), NULL);
302 MANAGE_WINDOW_SIGNALS_CONNECT(window);
303 gtk_signal_connect(GTK_OBJECT(ok_btn), "clicked",
304 GTK_SIGNAL_FUNC(prefs_actions_ok), mainwin);
305 gtk_signal_connect(GTK_OBJECT(cancel_btn), "clicked",
306 GTK_SIGNAL_FUNC(prefs_actions_cancel), NULL);
308 vbox1 = gtk_vbox_new(FALSE, 8);
309 gtk_widget_show(vbox1);
310 gtk_box_pack_start(GTK_BOX(vbox), vbox1, TRUE, TRUE, 0);
311 gtk_container_set_border_width(GTK_CONTAINER(vbox1), 2);
313 entry_vbox = gtk_vbox_new(FALSE, 4);
314 gtk_box_pack_start(GTK_BOX(vbox1), entry_vbox, FALSE, FALSE, 0);
316 hbox = gtk_hbox_new(FALSE, 8);
317 gtk_box_pack_start(GTK_BOX(entry_vbox), hbox, FALSE, FALSE, 0);
319 name_label = gtk_label_new(_("Menu name:"));
320 gtk_box_pack_start(GTK_BOX(hbox), name_label, FALSE, FALSE, 0);
322 name_entry = gtk_entry_new();
323 gtk_box_pack_start(GTK_BOX(hbox), name_entry, TRUE, TRUE, 0);
325 hbox = gtk_hbox_new(FALSE, 8);
326 gtk_box_pack_start(GTK_BOX(entry_vbox), hbox, TRUE, TRUE, 0);
328 cmd_label = gtk_label_new(_("Command line:"));
329 gtk_box_pack_start(GTK_BOX(hbox), cmd_label, FALSE, FALSE, 0);
331 cmd_entry = gtk_entry_new();
332 gtk_box_pack_start(GTK_BOX(hbox), cmd_entry, TRUE, TRUE, 0);
334 gtk_widget_show_all(entry_vbox);
336 /* register / substitute / delete */
338 reg_hbox = gtk_hbox_new(FALSE, 4);
339 gtk_widget_show(reg_hbox);
340 gtk_box_pack_start(GTK_BOX(vbox1), reg_hbox, FALSE, FALSE, 0);
342 arrow = gtk_arrow_new(GTK_ARROW_DOWN, GTK_SHADOW_OUT);
343 gtk_widget_show(arrow);
344 gtk_box_pack_start(GTK_BOX(reg_hbox), arrow, FALSE, FALSE, 0);
345 gtk_widget_set_usize(arrow, -1, 16);
347 btn_hbox = gtk_hbox_new(TRUE, 4);
348 gtk_widget_show(btn_hbox);
349 gtk_box_pack_start(GTK_BOX(reg_hbox), btn_hbox, FALSE, FALSE, 0);
351 reg_btn = gtk_button_new_with_label(_("Add"));
352 gtk_widget_show(reg_btn);
353 gtk_box_pack_start(GTK_BOX(btn_hbox), reg_btn, FALSE, TRUE, 0);
354 gtk_signal_connect(GTK_OBJECT(reg_btn), "clicked",
355 GTK_SIGNAL_FUNC(prefs_actions_register_cb), NULL);
357 subst_btn = gtk_button_new_with_label(_(" Replace "));
358 gtk_widget_show(subst_btn);
359 gtk_box_pack_start(GTK_BOX(btn_hbox), subst_btn, FALSE, TRUE, 0);
360 gtk_signal_connect(GTK_OBJECT(subst_btn), "clicked",
361 GTK_SIGNAL_FUNC(prefs_actions_substitute_cb),
364 del_btn = gtk_button_new_with_label(_("Delete"));
365 gtk_widget_show(del_btn);
366 gtk_box_pack_start(GTK_BOX(btn_hbox), del_btn, FALSE, TRUE, 0);
367 gtk_signal_connect(GTK_OBJECT(del_btn), "clicked",
368 GTK_SIGNAL_FUNC(prefs_actions_delete_cb), NULL);
370 help_button = gtk_button_new_with_label(_(" Syntax help "));
371 gtk_widget_show(help_button);
372 gtk_box_pack_end(GTK_BOX(reg_hbox), help_button, FALSE, FALSE, 0);
373 gtk_signal_connect(GTK_OBJECT(help_button), "clicked",
374 GTK_SIGNAL_FUNC(prefs_actions_help_cb), NULL);
376 cond_hbox = gtk_hbox_new(FALSE, 8);
377 gtk_widget_show(cond_hbox);
378 gtk_box_pack_start(GTK_BOX(vbox1), cond_hbox, TRUE, TRUE, 0);
380 cond_scrolledwin = gtk_scrolled_window_new(NULL, NULL);
381 gtk_widget_show(cond_scrolledwin);
382 gtk_widget_set_usize(cond_scrolledwin, -1, 150);
383 gtk_box_pack_start(GTK_BOX(cond_hbox), cond_scrolledwin,
385 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW (cond_scrolledwin),
386 GTK_POLICY_AUTOMATIC,
387 GTK_POLICY_AUTOMATIC);
389 title[0] = _("Current actions");
390 cond_clist = gtk_clist_new_with_titles(1, title);
391 gtk_widget_show(cond_clist);
392 gtk_container_add(GTK_CONTAINER (cond_scrolledwin), cond_clist);
393 gtk_clist_set_column_width(GTK_CLIST (cond_clist), 0, 80);
394 gtk_clist_set_selection_mode(GTK_CLIST (cond_clist),
395 GTK_SELECTION_BROWSE);
396 GTK_WIDGET_UNSET_FLAGS(GTK_CLIST(cond_clist)->column[0].button,
398 gtk_signal_connect(GTK_OBJECT(cond_clist), "select_row",
399 GTK_SIGNAL_FUNC(prefs_actions_select), NULL);
400 gtk_signal_connect_after(GTK_OBJECT(cond_clist), "row_move",
401 GTK_SIGNAL_FUNC(prefs_actions_row_move),
404 btn_vbox = gtk_vbox_new(FALSE, 8);
405 gtk_widget_show(btn_vbox);
406 gtk_box_pack_start(GTK_BOX(cond_hbox), btn_vbox, FALSE, FALSE, 0);
408 up_btn = gtk_button_new_with_label(_("Up"));
409 gtk_widget_show(up_btn);
410 gtk_box_pack_start(GTK_BOX(btn_vbox), up_btn, FALSE, FALSE, 0);
411 gtk_signal_connect(GTK_OBJECT(up_btn), "clicked",
412 GTK_SIGNAL_FUNC(prefs_actions_up), NULL);
414 down_btn = gtk_button_new_with_label(_("Down"));
415 gtk_widget_show(down_btn);
416 gtk_box_pack_start(GTK_BOX(btn_vbox), down_btn, FALSE, FALSE, 0);
417 gtk_signal_connect(GTK_OBJECT(down_btn), "clicked",
418 GTK_SIGNAL_FUNC(prefs_actions_down), NULL);
420 gtk_widget_show(window);
422 actions.window = window;
423 actions.ok_btn = ok_btn;
425 actions.name_entry = name_entry;
426 actions.cmd_entry = cmd_entry;
428 actions.actions_clist = cond_clist;
432 void prefs_actions_read_config(void)
436 gchar buf[PREFSBUFSIZE];
439 debug_print("Reading actions configurations...\n");
441 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, ACTIONS_RC, NULL);
442 if ((fp = fopen(rcpath, "rb")) == NULL) {
443 if (ENOENT != errno) FILE_OP_ERROR(rcpath, "fopen");
449 while (prefs_common.actions_list != NULL) {
450 act = (gchar *)prefs_common.actions_list->data;
451 prefs_common.actions_list =
452 g_slist_remove(prefs_common.actions_list, act);
456 while (fgets(buf, sizeof(buf), fp) != NULL) {
458 act = strstr(buf, ": ");
460 get_action_type(&act[2]) != ACTION_ERROR)
461 prefs_common.actions_list =
462 g_slist_append(prefs_common.actions_list,
468 void prefs_actions_write_config(void)
474 debug_print("Writing actions configuration...\n");
476 rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, ACTIONS_RC, NULL);
477 if ((pfile= prefs_write_open(rcpath)) == NULL) {
478 g_warning("failed to write configuration to file\n");
483 for (cur = prefs_common.actions_list; cur != NULL; cur = cur->next) {
484 gchar *act = (gchar *)cur->data;
485 if (fputs(act, pfile->fp) == EOF ||
486 fputc('\n', pfile->fp) == EOF) {
487 FILE_OP_ERROR(rcpath, "fputs || fputc");
488 prefs_file_close_revert(pfile);
496 if (prefs_file_close(pfile) < 0) {
497 g_warning("failed to write configuration to file\n");
502 static guint get_action_type(gchar *action)
505 guint action_type = ACTION_NONE;
507 g_return_val_if_fail(action, ACTION_ERROR);
508 g_return_val_if_fail(*action, ACTION_ERROR);
513 action_type |= ACTION_PIPE_IN;
515 } else if (p[0] == '>') {
516 action_type |= ACTION_OPEN_IN;
518 } else if (p[0] == '*') {
519 action_type |= ACTION_HIDE_IN;
526 while (*p && action_type != ACTION_ERROR) {
530 action_type |= ACTION_SINGLE;
533 action_type |= ACTION_MULTIPLE;
536 action_type |= ACTION_SINGLE;
539 action_type = ACTION_ERROR;
542 } else if (p[0] == '|') {
544 action_type |= ACTION_PIPE_OUT;
545 } else if (p[0] == '>') {
547 action_type |= ACTION_INSERT;
548 } else if (p[0] == '&') {
550 action_type |= ACTION_ASYNC;
552 action_type = ACTION_ERROR;
560 static gchar *parse_action_cmd(gchar *action, MsgInfo *msginfo,
561 GtkCTree *ctree, MimeView *mimeview)
570 if (p[0] == '|' || p[0] == '>' || p[0] == '*')
573 cmd = g_string_sized_new(strlen(action));
576 !((p[0] == '|' || p[0] == '>' || p[0] == '&') && !p[1])) {
577 if (p[0] == '%' && p[1]) {
580 if (!parse_append_filename(&cmd, msginfo)) {
581 g_string_free(cmd, TRUE);
587 for (cur = GTK_CLIST(ctree)->selection;
588 cur != NULL; cur = cur->next) {
589 msg = gtk_ctree_node_get_row_data(ctree,
590 GTK_CTREE_NODE(cur->data));
591 if (!parse_append_filename(&cmd, msg)) {
592 g_string_free(cmd, TRUE);
596 cmd = g_string_append_c(cmd, ' ');
601 if (!parse_append_msgpart(&cmd, msginfo,
603 g_string_free(cmd, TRUE);
609 cmd = g_string_append_c(cmd, p[0]);
610 cmd = g_string_append_c(cmd, p[1]);
614 cmd = g_string_append_c(cmd, p[0]);
619 g_string_free(cmd, TRUE);
624 g_string_free(cmd, FALSE);
628 static gboolean parse_append_filename(GString **cmd, MsgInfo *msginfo)
632 g_return_val_if_fail(msginfo, FALSE);
634 filename = procmsg_get_message_file(msginfo);
637 *cmd = g_string_append(*cmd, filename);
640 alertpanel_error(_("Could not get message file %d"),
648 static gboolean parse_append_msgpart(GString **cmd, MsgInfo *msginfo,
659 if ((fp = procmsg_open_message_decrypted(msginfo, &partinfo))
661 alertpanel_error(_("Could not get message file."));
665 if ((fp = procmsg_open_message(msginfo)) == NULL) {
666 alertpanel_error(_("Could not get message file."));
669 partinfo = procmime_scan_mime_header(fp);
673 procmime_mimeinfo_free_all(partinfo);
674 alertpanel_error(_("Could not get message part."));
677 filename = procmsg_get_message_file(msginfo);
679 if (!mimeview->opened) {
680 alertpanel_error(_("No message part selected."));
683 if (!mimeview->file) {
684 alertpanel_error(_("No message file selected."));
687 partinfo = gtk_ctree_node_get_row_data
688 (GTK_CTREE(mimeview->ctree),
690 g_return_val_if_fail(partinfo != NULL, FALSE);
691 filename = mimeview->file;
693 partname = procmime_get_tmp_file_name(partinfo);
695 ret = procmime_get_part(partname, filename, partinfo);
698 procmime_mimeinfo_free_all(partinfo);
703 alertpanel_error(_("Can't get part of multipart message"));
708 *cmd = g_string_append(*cmd, partname);
715 static void prefs_actions_set_dialog(void)
717 GtkCList *clist = GTK_CLIST(actions.actions_clist);
719 gchar *action_str[1];
722 gtk_clist_freeze(clist);
723 gtk_clist_clear(clist);
725 action_str[0] = _("(New)");
726 row = gtk_clist_append(clist, action_str);
727 gtk_clist_set_row_data(clist, row, NULL);
729 for (cur = prefs_common.actions_list; cur != NULL; cur = cur->next) {
732 action[0] = (gchar *)cur->data;
733 row = gtk_clist_append(clist, action);
734 gtk_clist_set_row_data(clist, row, action[0]);
737 gtk_clist_thaw(clist);
740 static void prefs_actions_set_list(void)
745 g_slist_free(prefs_common.actions_list);
746 prefs_common.actions_list = NULL;
748 while ((action = (gchar *)gtk_clist_get_row_data
749 (GTK_CLIST(actions.actions_clist), row)) != NULL) {
750 prefs_common.actions_list =
751 g_slist_append(prefs_common.actions_list, action);
756 #define GET_ENTRY(entry) \
757 entry_text = gtk_entry_get_text(GTK_ENTRY(entry))
759 static gint prefs_actions_clist_set_row(gint row)
761 GtkCList *clist = GTK_CLIST(actions.actions_clist);
764 gchar action[PREFSBUFSIZE];
767 g_return_val_if_fail(row != 0, -1);
769 GET_ENTRY(actions.name_entry);
770 if (entry_text[0] == '\0') {
771 alertpanel_error(_("Menu name is not set."));
775 if (strchr(entry_text, ':')) {
776 alertpanel_error(_("Colon ':' is not allowed in the menu name."));
780 strncpy(action, entry_text, PREFSBUFSIZE - 1);
783 /* Keep space for the ': ' delimiter */
784 len = strlen(action) + 2;
785 if (len >= PREFSBUFSIZE - 1) {
786 alertpanel_error(_("Menu name is too long."));
790 strcat(action, ": ");
792 GET_ENTRY(actions.cmd_entry);
794 if (entry_text[0] == '\0') {
795 alertpanel_error(_("Command line not set."));
799 if (len + strlen(entry_text) >= PREFSBUFSIZE - 1) {
800 alertpanel_error(_("Menu name and command are too long."));
804 if (get_action_type(entry_text) == ACTION_ERROR) {
805 alertpanel_error(_("The command\n%s\nhas a syntax error."),
810 strcat(action, entry_text);
814 row = gtk_clist_append(clist, buf);
817 gtk_clist_set_text(clist, row, 0, action);
818 old_action = (gchar *) gtk_clist_get_row_data(clist, row);
823 buf[0] = g_strdup(action);
825 gtk_clist_set_row_data(clist, row, buf[0]);
827 prefs_actions_set_list();
832 /* callback functions */
834 static void prefs_actions_register_cb(GtkWidget *w, gpointer data)
836 prefs_actions_clist_set_row(-1);
839 static void prefs_actions_substitute_cb(GtkWidget *w, gpointer data)
841 GtkCList *clist = GTK_CLIST(actions.actions_clist);
845 if (!clist->selection) return;
847 row = GPOINTER_TO_INT(clist->selection->data);
848 if (row == 0) return;
850 action = gtk_clist_get_row_data(clist, row);
853 prefs_actions_clist_set_row(row);
856 static void prefs_actions_delete_cb(GtkWidget *w, gpointer data)
858 GtkCList *clist = GTK_CLIST(actions.actions_clist);
862 if (!clist->selection) return;
863 row = GPOINTER_TO_INT(clist->selection->data);
864 if (row == 0) return;
866 if (alertpanel(_("Delete action"),
867 _("Do you really want to delete this action?"),
868 _("Yes"), _("No"), NULL) == G_ALERTALTERNATE)
871 action = gtk_clist_get_row_data(clist, row);
873 gtk_clist_remove(clist, row);
874 prefs_common.actions_list = g_slist_remove(prefs_common.actions_list,
878 static void prefs_actions_up(GtkWidget *w, gpointer data)
880 GtkCList *clist = GTK_CLIST(actions.actions_clist);
883 if (!clist->selection) return;
885 row = GPOINTER_TO_INT(clist->selection->data);
887 gtk_clist_row_move(clist, row, row - 1);
890 static void prefs_actions_down(GtkWidget *w, gpointer data)
892 GtkCList *clist = GTK_CLIST(actions.actions_clist);
895 if (!clist->selection) return;
897 row = GPOINTER_TO_INT(clist->selection->data);
898 if (row > 0 && row < clist->rows - 1)
899 gtk_clist_row_move(clist, row, row + 1);
902 #define ENTRY_SET_TEXT(entry, str) \
903 gtk_entry_set_text(GTK_ENTRY(entry), str ? str : "")
905 static void prefs_actions_select(GtkCList *clist, gint row, gint column,
910 gchar buf[PREFSBUFSIZE];
911 action = gtk_clist_get_row_data(clist, row);
914 ENTRY_SET_TEXT(actions.name_entry, "");
915 ENTRY_SET_TEXT(actions.cmd_entry, "");
919 strncpy(buf, action, PREFSBUFSIZE - 1);
920 buf[PREFSBUFSIZE - 1] = 0x00;
921 cmd = strstr(buf, ": ");
924 ENTRY_SET_TEXT(actions.cmd_entry, &cmd[2]);
929 ENTRY_SET_TEXT(actions.name_entry, buf);
932 static void prefs_actions_row_move(GtkCList *clist,
933 gint source_row, gint dest_row)
935 prefs_actions_set_list();
936 if (gtk_clist_row_is_visible(clist, dest_row) != GTK_VISIBILITY_FULL) {
937 gtk_clist_moveto(clist, dest_row, -1,
938 source_row < dest_row ? 1.0 : 0.0, 0.0);
942 static gint prefs_actions_deleted(GtkWidget *widget, GdkEventAny *event,
945 prefs_actions_cancel(widget, data);
949 static void prefs_actions_key_pressed(GtkWidget *widget, GdkEventKey *event,
952 if (event && event->keyval == GDK_Escape)
953 prefs_actions_cancel(widget, data);
956 static void prefs_actions_cancel(GtkWidget *w, gpointer data)
958 prefs_actions_read_config();
959 gtk_widget_hide(actions.window);
963 static void prefs_actions_ok(GtkWidget *widget, gpointer data)
965 GtkItemFactory *ifactory;
966 MainWindow *mainwin = (MainWindow *)data;
968 prefs_actions_write_config();
969 ifactory = gtk_item_factory_from_widget(mainwin->menubar);
970 update_mainwin_actions_menu(ifactory, mainwin);
971 gtk_widget_hide(actions.window);
975 void update_mainwin_actions_menu(GtkItemFactory *ifactory,
978 update_actions_menu(ifactory, "/Tools/Actions",
979 mainwin_actions_execute_cb,
983 void update_compose_actions_menu(GtkItemFactory *ifactory,
987 update_actions_menu(ifactory, branch_path,
988 compose_actions_execute_cb,
993 void actions_execute(gpointer data,
998 if (source == TOOLBAR_MAIN)
999 mainwin_actions_execute_cb((MainWindow*)data, action_nb, widget);
1000 else if (source == TOOLBAR_COMPOSE)
1001 compose_actions_execute_cb((Compose*)data, action_nb, widget);
1002 else if (source == TOOLBAR_MSGVIEW)
1003 msgview_actions_execute_cb((MessageView*)data, action_nb, widget);
1007 static void update_actions_menu(GtkItemFactory *ifactory,
1012 GtkWidget *menuitem;
1015 gchar *action, *action_p;
1017 GtkItemFactoryEntry ifentry = {NULL, NULL, NULL, 0, "<Branch>"};
1019 ifentry.path = branch_path;
1020 menuitem = gtk_item_factory_get_widget(ifactory, branch_path);
1021 g_return_if_fail(menuitem != NULL);
1023 amenu = GTK_MENU_SHELL(menuitem)->children;
1024 while (amenu != NULL) {
1025 GList *alist = amenu->next;
1026 gtk_widget_destroy(GTK_WIDGET(amenu->data));
1030 ifentry.accelerator = NULL;
1031 ifentry.callback_action = 0;
1032 ifentry.callback = callback;
1033 ifentry.item_type = NULL;
1035 for (cur = prefs_common.actions_list; cur; cur = cur->next) {
1036 action = g_strdup((gchar *)cur->data);
1037 action_p = strstr(action, ": ");
1038 if (action_p && action_p[2] &&
1039 get_action_type(&action_p[2]) != ACTION_ERROR) {
1041 menu_path = g_strdup_printf("%s/%s", branch_path,
1043 ifentry.path = menu_path;
1044 gtk_item_factory_create_item(ifactory, &ifentry, data,
1049 ifentry.callback_action++;
1053 static void compose_actions_execute_cb(Compose *compose, guint action_nb,
1056 gchar *buf, *action;
1059 g_return_if_fail(action_nb < g_slist_length(prefs_common.actions_list));
1061 buf = (gchar *)g_slist_nth_data(prefs_common.actions_list, action_nb);
1062 g_return_if_fail(buf != NULL);
1063 action = strstr(buf, ": ");
1064 g_return_if_fail(action != NULL);
1066 /* Point to the beginning of the command-line */
1069 action_type = get_action_type(action);
1070 if (action_type & (ACTION_SINGLE | ACTION_MULTIPLE)) {
1072 (_("The selected action cannot be used in the compose window\n"
1073 "because it contains %%f, %%F or %%p."));
1077 execute_actions(action, compose->window, NULL, compose->text, NULL, 0,
1081 static void mainwin_actions_execute_cb(MainWindow *mainwin, guint action_nb,
1084 MessageView *messageview = mainwin->messageview;
1085 TextView *textview = NULL;
1088 MimeView *mimeview = NULL;
1090 g_return_if_fail(action_nb < g_slist_length(prefs_common.actions_list));
1092 buf = (gchar *)g_slist_nth_data(prefs_common.actions_list, action_nb);
1094 g_return_if_fail(buf);
1095 g_return_if_fail(action = strstr(buf, ": "));
1097 /* Point to the beginning of the command-line */
1100 switch (messageview->type) {
1102 if (messageview->textview && messageview->textview->text)
1103 textview = messageview->textview;
1106 if (messageview->mimeview) {
1107 mimeview = messageview->mimeview;
1108 if (messageview->mimeview->type == MIMEVIEW_TEXT &&
1109 messageview->mimeview->textview &&
1110 messageview->mimeview->textview->text)
1111 textview = messageview->mimeview->textview;
1116 execute_actions(action, mainwin->window,
1117 GTK_CTREE(mainwin->summaryview->ctree), textview->text,
1118 textview->msgfont, textview->body_pos, mimeview);
1121 /* FIXME: Code duplication mainwindow_actions_execute_cb
1123 static void msgview_actions_execute_cb(MessageView *msgview, guint action_nb,
1126 TextView *textview = NULL;
1129 MimeView *mimeview = NULL;
1131 g_return_if_fail(action_nb < g_slist_length(prefs_common.actions_list));
1133 buf = (gchar *)g_slist_nth_data(prefs_common.actions_list, action_nb);
1135 g_return_if_fail(buf);
1136 g_return_if_fail(action = strstr(buf, ": "));
1138 /* Point to the beginning of the command-line */
1141 switch (msgview->type) {
1143 if (msgview->textview && msgview->textview->text)
1144 textview = msgview->textview;
1147 if (msgview->mimeview) {
1148 mimeview = msgview->mimeview;
1149 if (msgview->mimeview->type == MIMEVIEW_TEXT &&
1150 msgview->mimeview->textview &&
1151 msgview->mimeview->textview->text)
1152 textview = msgview->mimeview->textview;
1157 execute_actions(action, msgview->window, NULL, textview->text,
1158 textview->msgfont, textview->body_pos, mimeview);
1161 static gboolean execute_actions(gchar *action, GtkWidget *window,
1162 GtkCTree *ctree, GtkWidget *text,
1163 GdkFont *msgfont, gint body_pos,
1166 GList *cur, *selection = NULL;
1167 GSList *children_list = NULL;
1169 gint selection_len = 0;
1171 ChildInfo *child_info;
1176 g_return_val_if_fail(action && *action, FALSE);
1178 action_type = get_action_type(action);
1180 if (action_type == ACTION_ERROR)
1181 return FALSE; /* ERR: syntax error */
1183 if (action_type & (ACTION_SINGLE | ACTION_MULTIPLE) &&
1184 !(ctree && GTK_CLIST(ctree)->selection))
1185 return FALSE; /* ERR: file command without selection */
1188 selection = GTK_CLIST(ctree)->selection;
1189 selection_len = g_list_length(selection);
1192 if (action_type & (ACTION_PIPE_OUT | ACTION_PIPE_IN | ACTION_INSERT)) {
1193 if (ctree && selection_len > 1)
1194 return FALSE; /* ERR: pipe + multiple selection */
1196 return FALSE; /* ERR: pipe and no displayed text */
1199 children = g_new0(Children, 1);
1201 if (action_type & ACTION_SINGLE) {
1202 for (cur = selection; cur && is_ok == TRUE; cur = cur->next) {
1203 msginfo = gtk_ctree_node_get_row_data(ctree,
1204 GTK_CTREE_NODE(cur->data));
1206 is_ok = FALSE; /* ERR: msginfo missing */
1209 cmd = parse_action_cmd(action, msginfo, ctree,
1212 debug_print("Action command error\n");
1213 is_ok = FALSE; /* ERR: incorrect command */
1216 if ((child_info = fork_child(cmd, action_type, text,
1219 children_list = g_slist_append(children_list,
1221 children->open_in = (selection_len == 1) ?
1224 ACTION_HIDE_IN)) : 0;
1229 cmd = parse_action_cmd(action, NULL, ctree, mimeview);
1231 if ((child_info = fork_child(cmd, action_type, text,
1234 children_list = g_slist_append(children_list,
1236 children->open_in = action_type &
1242 is_ok = FALSE; /* ERR: incorrect command */
1245 if (!children_list) {
1246 /* If not waiting for children, return */
1251 children->action = g_strdup(action);
1252 children->window = window;
1253 children->dialog = NULL;
1254 children->list = children_list;
1255 children->nb = g_slist_length(children_list);
1257 for (cur = children_list; cur; cur = cur->next) {
1258 child_info = (ChildInfo *) cur->data;
1259 child_info->tag_status =
1260 gdk_input_add(child_info->chld_status,
1262 catch_status, child_info);
1265 create_io_dialog(children);
1271 ChildInfo *fork_child(gchar *cmd, gint action_type, GtkWidget *text,
1272 GdkFont *msgfont, gint body_pos, Children *children)
1274 gint chld_in[2], chld_out[2], chld_err[2], chld_status[2];
1276 gint start, end, is_selection;
1279 ChildInfo *child_info;
1282 sync = !(action_type & ACTION_ASYNC);
1284 chld_in[0] = chld_in[1] = chld_out[0] = chld_out[1] = chld_err[0]
1285 = chld_err[1] = chld_status[0] = chld_status[1] = -1;
1288 if (pipe(chld_status) || pipe(chld_in) || pipe(chld_out) ||
1290 alertpanel_error(_("Command could not be started. "
1291 "Pipe creation failed.\n%s"),
1293 /* Closing fd = -1 fails silently */
1300 close(chld_status[0]);
1301 close(chld_status[1]);
1302 return NULL; /* Pipe error */
1306 debug_print("Forking child and grandchild.\n");
1309 if (pid == 0) { /* Child */
1313 close(ConnectionNumber(gdk_display));
1318 if (setpgid(0, getppid()))
1325 close(fileno(stdin));
1331 close(fileno(stdout));
1336 close(fileno(stderr));
1346 execvp("/bin/sh", cmdline);
1350 } else if (gch_pid < (pid_t) 0) { /* Fork error */
1352 write(chld_status[1], "1\n", 2);
1363 close(chld_status[0]);
1366 debug_print("Child: Waiting for grandchild\n");
1367 waitpid(gch_pid, NULL, 0);
1368 debug_print("Child: grandchild ended\n");
1369 write(chld_status[1], "0\n", 2);
1370 close(chld_status[1]);
1374 } else if (pid < 0) { /* Fork error */
1375 alertpanel_error(_("Could not fork to execute the following "
1376 "command:\n%s\n%s"),
1377 cmd, g_strerror(errno));
1384 waitpid(pid, NULL, 0);
1389 if (!(action_type & (ACTION_PIPE_IN | ACTION_OPEN_IN | ACTION_HIDE_IN)))
1393 close(chld_status[1]);
1395 child_info = g_new0(ChildInfo, 1);
1397 child_info->children = children;
1399 child_info->pid = pid;
1400 child_info->cmd = g_strdup(cmd);
1401 child_info->type = action_type;
1402 child_info->new_out = FALSE;
1403 child_info->output = g_string_sized_new(0);
1404 child_info->chld_in =
1406 (ACTION_PIPE_IN | ACTION_OPEN_IN | ACTION_HIDE_IN))
1408 child_info->chld_out = chld_out[0];
1409 child_info->chld_err = chld_err[0];
1410 child_info->chld_status = chld_status[0];
1411 child_info->tag_in = -1;
1412 child_info->tag_out = gdk_input_add(chld_out[0], GDK_INPUT_READ,
1413 catch_output, child_info);
1414 child_info->tag_err = gdk_input_add(chld_err[0], GDK_INPUT_READ,
1415 catch_output, child_info);
1417 if (!(action_type & (ACTION_PIPE_IN | ACTION_PIPE_OUT | ACTION_INSERT)))
1420 child_info->text = text;
1421 child_info->msgfont = msgfont;
1424 end = gtk_stext_get_length(GTK_STEXT(text));
1426 if (GTK_EDITABLE(text)->has_selection) {
1427 start = GTK_EDITABLE(text)->selection_start_pos;
1428 end = GTK_EDITABLE(text)->selection_end_pos;
1435 is_selection = TRUE;
1438 end = gtk_stext_get_length(GTK_STEXT(text));
1439 is_selection = FALSE;
1443 selection = gtk_editable_get_chars(GTK_EDITABLE(text), start, end);
1445 if (action_type & ACTION_PIPE_IN) {
1446 write(chld_in[1], selection, strlen(selection));
1447 if (!(action_type & (ACTION_OPEN_IN | ACTION_HIDE_IN)))
1449 child_info->chld_in = -1; /* No more input */
1453 gtk_stext_freeze(GTK_STEXT(text));
1454 if (action_type & ACTION_PIPE_OUT) {
1455 gtk_stext_set_point(GTK_STEXT(text), start);
1456 gtk_stext_forward_delete(GTK_STEXT(text), end - start);
1459 gtk_stext_thaw(GTK_STEXT(text));
1464 static void kill_children_cb(GtkWidget *widget, gpointer data)
1467 Children *children = (Children *) data;
1468 ChildInfo *child_info;
1470 for (cur = children->list; cur; cur = cur->next) {
1471 child_info = (ChildInfo *)(cur->data);
1472 debug_print("Killing child group id %d\n", child_info->pid);
1473 if (child_info->pid && kill(-child_info->pid, SIGTERM) < 0)
1478 static gint wait_for_children(gpointer data)
1480 gboolean new_output;
1481 Children *children = (Children *)data;
1482 ChildInfo *child_info;
1484 gint nb = children->nb;
1488 cur = children->list;
1491 child_info = (ChildInfo *)cur->data;
1492 if (child_info->pid)
1494 new_output |= child_info->new_out;
1498 children->output |= new_output;
1500 if (new_output || (children->dialog && (nb != children->nb)))
1501 update_io_dialog(children);
1506 if (!children->dialog) {
1507 free_children(children);
1508 } else if (!children->output) {
1509 gtk_widget_destroy(children->dialog);
1515 static void send_input(GtkWidget *w, gpointer data)
1517 Children *children = (Children *) data;
1518 ChildInfo *child_info = (ChildInfo *) children->list->data;
1520 child_info->tag_in = gdk_input_add(child_info->chld_in,
1522 catch_input, children);
1523 gtk_widget_set_sensitive(children->input_hbox, FALSE);
1526 static gint delete_io_dialog_cb(GtkWidget *w, GdkEvent *e, gpointer data)
1528 hide_io_dialog_cb(w, data);
1532 static void hide_io_dialog_cb(GtkWidget *w, gpointer data)
1535 Children *children = (Children *)data;
1537 if (!children->nb) {
1538 gtk_signal_disconnect_by_data(GTK_OBJECT(children->dialog),
1540 gtk_widget_destroy(children->dialog);
1541 free_children(children);
1545 static void childinfo_close_pipes(ChildInfo *child_info)
1547 if (child_info->tag_in > 0)
1548 gdk_input_remove(child_info->tag_in);
1549 gdk_input_remove(child_info->tag_out);
1550 gdk_input_remove(child_info->tag_err);
1552 if (child_info->chld_in >= 0)
1553 close(child_info->chld_in);
1554 close(child_info->chld_out);
1555 close(child_info->chld_err);
1556 close(child_info->chld_status);
1559 static void free_children(Children *children)
1562 ChildInfo *child_info;
1564 debug_print("Freeing children data %p\n", children);
1566 g_free(children->action);
1567 for (cur = children->list; cur;) {
1568 child_info = (ChildInfo *)cur->data;
1569 g_free(child_info->cmd);
1570 g_string_free(child_info->output, TRUE);
1571 children->list = g_slist_remove(children->list, child_info);
1573 cur = children->list;
1578 static void update_io_dialog(Children *children)
1582 debug_print("Updating actions input/output dialog.\n");
1584 if (!children->nb) {
1585 gtk_widget_set_sensitive(children->abort_btn, FALSE);
1586 gtk_widget_set_sensitive(children->close_btn, TRUE);
1587 if (children->input_hbox)
1588 gtk_widget_set_sensitive(children->input_hbox, FALSE);
1591 if (children->output) {
1592 GtkWidget *text = children->text;
1594 ChildInfo *child_info;
1596 gtk_widget_show(children->scrolledwin);
1597 gtk_text_freeze(GTK_TEXT(text));
1598 gtk_text_set_point(GTK_TEXT(text), 0);
1599 gtk_text_forward_delete(GTK_TEXT(text),
1600 gtk_text_get_length(GTK_TEXT(text)));
1601 for (cur = children->list; cur; cur = cur->next) {
1602 child_info = (ChildInfo *)cur->data;
1603 if (child_info->pid)
1604 caption = g_strdup_printf
1605 (_("--- Running: %s\n"),
1608 caption = g_strdup_printf
1609 (_("--- Ended: %s\n"),
1612 gtk_text_insert(GTK_TEXT(text), NULL, NULL, NULL,
1614 gtk_text_insert(GTK_TEXT(text), NULL, NULL, NULL,
1615 child_info->output->str, -1);
1617 child_info->new_out = FALSE;
1619 gtk_text_thaw(GTK_TEXT(text));
1623 static void create_io_dialog(Children *children)
1627 GtkWidget *entry = NULL;
1628 GtkWidget *input_hbox = NULL;
1629 GtkWidget *send_button;
1632 GtkWidget *scrolledwin;
1634 GtkWidget *abort_button;
1635 GtkWidget *close_button;
1637 debug_print("Creating action IO dialog\n");
1639 dialog = gtk_dialog_new();
1640 gtk_container_set_border_width
1641 (GTK_CONTAINER(GTK_DIALOG(dialog)->action_area), 5);
1642 gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER);
1643 gtk_window_set_title(GTK_WINDOW(dialog), _("Action's input/output"));
1644 gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
1645 manage_window_set_transient(GTK_WINDOW(dialog));
1646 gtk_signal_connect(GTK_OBJECT(dialog), "delete_event",
1647 GTK_SIGNAL_FUNC(delete_io_dialog_cb), children);
1648 gtk_signal_connect(GTK_OBJECT(dialog), "destroy",
1649 GTK_SIGNAL_FUNC(hide_io_dialog_cb),
1652 vbox = gtk_vbox_new(FALSE, 8);
1653 gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox), vbox);
1654 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
1655 gtk_widget_show(vbox);
1657 label = gtk_label_new(children->action);
1658 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
1659 gtk_widget_show(label);
1661 scrolledwin = gtk_scrolled_window_new(NULL, NULL);
1662 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin),
1663 GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
1664 gtk_box_pack_start(GTK_BOX(vbox), scrolledwin, TRUE, TRUE, 0);
1665 gtk_widget_set_usize(scrolledwin, 480, 200);
1666 gtk_widget_hide(scrolledwin);
1668 text = gtk_text_new(gtk_scrolled_window_get_hadjustment
1669 (GTK_SCROLLED_WINDOW(scrolledwin)),
1670 gtk_scrolled_window_get_vadjustment
1671 (GTK_SCROLLED_WINDOW(scrolledwin)));
1672 gtk_text_set_editable(GTK_TEXT(text), FALSE);
1673 gtk_container_add(GTK_CONTAINER(scrolledwin), text);
1674 gtk_widget_show(text);
1676 if (children->open_in) {
1677 input_hbox = gtk_hbox_new(FALSE, 8);
1678 gtk_widget_show(input_hbox);
1680 entry = gtk_entry_new();
1681 gtk_widget_set_usize(entry, 320, -1);
1682 gtk_signal_connect(GTK_OBJECT(entry), "activate",
1683 GTK_SIGNAL_FUNC(send_input), children);
1684 gtk_box_pack_start(GTK_BOX(input_hbox), entry, TRUE, TRUE, 0);
1685 if (children->open_in & ACTION_HIDE_IN)
1686 gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE);
1687 gtk_widget_show(entry);
1689 send_button = gtk_button_new_with_label(_(" Send "));
1690 gtk_signal_connect(GTK_OBJECT(send_button), "clicked",
1691 GTK_SIGNAL_FUNC(send_input), children);
1692 gtk_box_pack_start(GTK_BOX(input_hbox), send_button, FALSE,
1694 gtk_widget_show(send_button);
1696 gtk_box_pack_start(GTK_BOX(vbox), input_hbox, FALSE, FALSE, 0);
1697 gtk_widget_grab_focus(entry);
1700 gtkut_button_set_create(&hbox, &abort_button, _("Abort"),
1701 &close_button, _("Close"), NULL, NULL);
1702 gtk_signal_connect(GTK_OBJECT(abort_button), "clicked",
1703 GTK_SIGNAL_FUNC(kill_children_cb), children);
1704 gtk_signal_connect(GTK_OBJECT(close_button), "clicked",
1705 GTK_SIGNAL_FUNC(hide_io_dialog_cb), children);
1706 gtk_widget_show(hbox);
1709 gtk_widget_set_sensitive(close_button, FALSE);
1711 gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->action_area), hbox);
1713 children->dialog = dialog;
1714 children->scrolledwin = scrolledwin;
1715 children->text = text;
1716 children->input_hbox = children->open_in ? input_hbox : NULL;
1717 children->input_entry = children->open_in ? entry : NULL;
1718 children->abort_btn = abort_button;
1719 children->close_btn = close_button;
1721 gtk_widget_show(dialog);
1724 static void catch_status(gpointer data, gint source, GdkInputCondition cond)
1726 ChildInfo *child_info = (ChildInfo *)data;
1730 gdk_input_remove(child_info->tag_status);
1732 c = read(source, &buf, 1);
1733 debug_print("Child returned %c\n", buf);
1735 waitpid(-child_info->pid, NULL, 0);
1736 childinfo_close_pipes(child_info);
1737 child_info->pid = 0;
1739 wait_for_children(child_info->children);
1742 static void catch_input(gpointer data, gint source, GdkInputCondition cond)
1744 Children *children = (Children *)data;
1745 ChildInfo *child_info = (ChildInfo *)children->list->data;
1749 debug_print("Sending input to grand child.\n");
1750 if (!(cond && GDK_INPUT_WRITE))
1753 gdk_input_remove(child_info->tag_in);
1754 child_info->tag_in = -1;
1756 input = gtk_editable_get_chars(GTK_EDITABLE(children->input_entry),
1758 c = write(child_info->chld_in, input, strlen(input));
1762 write(child_info->chld_in, "\n", 2);
1764 gtk_entry_set_text(GTK_ENTRY(children->input_entry), "");
1765 gtk_widget_set_sensitive(children->input_hbox, TRUE);
1766 debug_print("Input to grand child sent.\n");
1769 static void catch_output(gpointer data, gint source, GdkInputCondition cond)
1771 ChildInfo *child_info = (ChildInfo *)data;
1773 gchar buf[PREFSBUFSIZE];
1775 debug_print("Catching grand child's output.\n");
1776 if (child_info->type & (ACTION_PIPE_OUT | ACTION_INSERT)
1777 && source == child_info->chld_out) {
1778 gboolean is_selection = FALSE;
1779 GtkWidget *text = child_info->text;
1780 if (GTK_EDITABLE(text)->has_selection)
1781 is_selection = TRUE;
1782 gtk_stext_freeze(GTK_STEXT(text));
1784 c = read(source, buf, PREFSBUFSIZE - 1);
1787 gtk_stext_insert(GTK_STEXT(text), child_info->msgfont,
1788 NULL, NULL, buf, c);
1791 /* Using the select_region draws things. Should not.
1792 * so we just change selection position and
1793 * defere drawing when thawing. Hack?
1795 GTK_EDITABLE(text)->selection_end_pos =
1796 gtk_stext_get_point(GTK_STEXT(text));
1798 gtk_stext_thaw(GTK_STEXT(child_info->text));
1800 c = read(source, buf, PREFSBUFSIZE - 1);
1801 for (i = 0; i < c; i++)
1802 child_info->output = g_string_append_c
1803 (child_info->output, buf[i]);
1805 child_info->new_out = TRUE;
1807 wait_for_children(child_info->children);
1811 * Strings describing action format strings
1813 * When adding new lines, remember to put one string for each line
1815 static gchar *actions_desc_strings[] = {
1817 N_(" Use / in menu name to make submenus."),
1819 N_("Command line:"),
1821 N_(" | to send message body or selection to command"),
1822 N_(" > to send user provided text to command"),
1823 N_(" * to send user provided hidden text to command"),
1825 N_(" | to replace message body or selection with command output"),
1826 N_(" > to insert command's output without replacing old text"),
1827 N_(" & to run command asynchronously"),
1828 N_(" Use %f for message file name"),
1829 N_(" %F for the list of the file names of selected messages"),
1830 N_(" %p for the selected message MIME part."),
1835 static DescriptionWindow actions_desc_win = {
1838 N_("Description of symbols"),
1839 actions_desc_strings
1843 static void prefs_actions_help_cb(GtkWidget *w, gpointer data)
1845 description_window_create(&actions_desc_win);