1 /* Python plugin for Claws-Mail
2 * Copyright (C) 2009 Holger Berndt
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #include "claws-features.h"
24 #include <glib/gi18n.h>
30 #include "common/hooks.h"
31 #include "common/plugin.h"
32 #include "common/version.h"
33 #include "common/utils.h"
36 #include "mainwindow.h"
37 #include "prefs_toolbar.h"
39 #include "python-shell.h"
40 #include "python-hooks.h"
41 #include "clawsmailmodule.h"
43 #define PYTHON_SCRIPTS_BASE_DIR "python-scripts"
44 #define PYTHON_SCRIPTS_MAIN_DIR "main"
45 #define PYTHON_SCRIPTS_COMPOSE_DIR "compose"
46 #define PYTHON_SCRIPTS_AUTO_DIR "auto"
47 #define PYTHON_SCRIPTS_AUTO_STARTUP "startup"
48 #define PYTHON_SCRIPTS_AUTO_SHUTDOWN "shutdown"
49 #define PYTHON_SCRIPTS_AUTO_COMPOSE "compose_any"
50 #define PYTHON_SCRIPTS_ACTION_PREFIX "Tools/PythonScripts/"
52 static GSList *menu_id_list = NULL;
53 static GSList *python_mainwin_scripts_id_list = NULL;
54 static GSList *python_mainwin_scripts_names = NULL;
55 static GSList *python_compose_scripts_names = NULL;
57 static GtkWidget *python_console = NULL;
59 static guint hook_compose_create;
61 static gboolean python_console_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
64 GtkToggleAction *action;
66 mainwin = mainwindow_get_mainwindow();
67 action = GTK_TOGGLE_ACTION(gtk_action_group_get_action(mainwin->action_group, "Tools/ShowPythonConsole"));
68 gtk_toggle_action_set_active(action, FALSE);
72 static void setup_python_console(void)
77 python_console = gtk_window_new(GTK_WINDOW_TOPLEVEL);
78 gtk_widget_set_size_request(python_console, 600, 400);
80 vbox = gtk_vbox_new(FALSE, 0);
81 gtk_container_add(GTK_CONTAINER(python_console), vbox);
83 console = parasite_python_shell_new();
84 gtk_box_pack_start(GTK_BOX(vbox), console, TRUE, TRUE, 0);
86 g_signal_connect(python_console, "delete-event", G_CALLBACK(python_console_delete_event), NULL);
88 gtk_widget_show_all(python_console);
90 parasite_python_shell_focus(PARASITE_PYTHON_SHELL(console));
93 static void show_hide_python_console(GtkToggleAction *action, gpointer callback_data)
95 if(gtk_toggle_action_get_active(action)) {
97 setup_python_console();
98 gtk_widget_show(python_console);
101 gtk_widget_hide(python_console);
105 static void remove_python_scripts_menus(void)
110 mainwin = mainwindow_get_mainwindow();
113 for(walk = python_mainwin_scripts_names; walk; walk = walk->next)
114 prefs_toolbar_unregister_plugin_item(TOOLBAR_MAIN, "Python", walk->data);
117 for(walk = python_mainwin_scripts_id_list; walk; walk = walk->next)
118 gtk_ui_manager_remove_ui(mainwin->ui_manager, GPOINTER_TO_UINT(walk->data));
119 g_slist_free(python_mainwin_scripts_id_list);
120 python_mainwin_scripts_id_list = NULL;
123 for(walk = python_mainwin_scripts_names; walk; walk = walk->next) {
126 entry = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, walk->data, NULL);
127 action = gtk_action_group_get_action(mainwin->action_group, entry);
130 gtk_action_group_remove_action(mainwin->action_group, action);
133 g_slist_free(python_mainwin_scripts_names);
134 python_mainwin_scripts_names = NULL;
136 /* compose scripts */
137 for(walk = python_compose_scripts_names; walk; walk = walk->next) {
138 prefs_toolbar_unregister_plugin_item(TOOLBAR_COMPOSE, "Python", walk->data);
141 g_slist_free(python_compose_scripts_names);
142 python_compose_scripts_names = NULL;
145 static gchar* extract_filename(const gchar *str)
149 filename = g_strrstr(str, "/");
150 if(!filename || *(filename+1) == '\0') {
151 debug_print("Error: Could not extract filename from %s\n", str);
158 static void run_script_file(const gchar *filename, Compose *compose)
161 fp = fopen(filename, "r");
163 g_print("Error: Could not open file '%s'\n", filename);
166 put_composewindow_into_module(compose);
167 PyRun_SimpleFile(fp, filename);
171 static void run_auto_script_file_if_it_exists(const gchar *autofilename, Compose *compose)
173 gchar *auto_filepath;
175 /* execute auto/autofilename, if it exists */
176 auto_filepath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
177 PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
178 PYTHON_SCRIPTS_AUTO_DIR, G_DIR_SEPARATOR_S, autofilename, NULL);
179 if(file_exist(auto_filepath, FALSE))
180 run_script_file(auto_filepath, compose);
181 g_free(auto_filepath);
184 static void python_mainwin_script_callback(GtkAction *action, gpointer data)
188 filename = extract_filename(data);
191 filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_MAIN_DIR, G_DIR_SEPARATOR_S, filename, NULL);
192 run_script_file(filename, NULL);
196 typedef struct _ComposeActionData ComposeActionData;
197 struct _ComposeActionData {
202 static void python_compose_script_callback(GtkAction *action, gpointer data)
205 ComposeActionData *dat = (ComposeActionData*)data;
207 filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_COMPOSE_DIR, G_DIR_SEPARATOR_S, dat->name, NULL);
208 run_script_file(filename, dat->compose);
213 static void mainwin_toolbar_callback(gpointer parent, const gchar *item_name, gpointer data)
216 script = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, item_name, NULL);
217 python_mainwin_script_callback(NULL, script);
221 static void compose_toolbar_callback(gpointer parent, const gchar *item_name, gpointer data)
225 filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
226 PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
227 PYTHON_SCRIPTS_COMPOSE_DIR, G_DIR_SEPARATOR_S,
229 run_script_file(filename, (Compose*)parent);
233 static char* make_sure_script_directory_exists(const gchar *subdir)
237 dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, subdir, NULL);
238 if(!g_file_test(dir, G_FILE_TEST_IS_DIR)) {
239 if(g_mkdir(dir, 0777) != 0)
240 retval = g_strdup_printf("Could not create directory '%s': %s", dir, g_strerror(errno));
246 static int make_sure_directories_exist(char **error)
248 const char* dirs[] = {
250 , PYTHON_SCRIPTS_MAIN_DIR
251 , PYTHON_SCRIPTS_COMPOSE_DIR
252 , PYTHON_SCRIPTS_AUTO_DIR
255 const char **dir = dirs;
260 *error = make_sure_script_directory_exists(*dir);
266 return (*error == NULL);
269 static void migrate_scripts_out_of_base_dir(void)
273 const char *filename;
276 base_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, NULL);
277 dir = g_dir_open(base_dir, 0, NULL);
282 dest_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
283 PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
284 PYTHON_SCRIPTS_MAIN_DIR, NULL);
285 if(!g_file_test(dest_dir, G_FILE_TEST_IS_DIR)) {
286 if(g_mkdir(dest_dir, 0777) != 0) {
293 while((filename = g_dir_read_name(dir)) != NULL) {
295 filepath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, filename, NULL);
296 if(g_file_test(filepath, G_FILE_TEST_IS_REGULAR)) {
298 dest_file = g_strconcat(dest_dir, G_DIR_SEPARATOR_S, filename, NULL);
299 if(move_file(filepath, dest_file, FALSE) == 0)
300 g_print("Python plugin: Moved file '%s' to %s subdir\n", filename, PYTHON_SCRIPTS_MAIN_DIR);
302 g_print("Python plugin: Warning: Could not move file '%s' to %s subdir\n", filename, PYTHON_SCRIPTS_MAIN_DIR);
312 static void create_mainwindow_menus_and_items(GSList *filenames, gint num_entries)
317 GtkActionEntry *entries;
319 /* create menu items */
320 entries = g_new0(GtkActionEntry, num_entries);
322 mainwin = mainwindow_get_mainwindow();
323 for(walk = filenames; walk; walk = walk->next) {
324 entries[ii].name = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, walk->data, NULL);
325 entries[ii].label = walk->data;
326 entries[ii].callback = G_CALLBACK(python_mainwin_script_callback);
327 gtk_action_group_add_actions(mainwin->action_group, &(entries[ii]), 1, (gpointer)entries[ii].name);
330 for(ii = 0; ii < num_entries; ii++) {
333 python_mainwin_scripts_names = g_slist_prepend(python_mainwin_scripts_names, g_strdup(entries[ii].label));
334 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/" PYTHON_SCRIPTS_ACTION_PREFIX, entries[ii].label,
335 entries[ii].name, GTK_UI_MANAGER_MENUITEM, id)
336 python_mainwin_scripts_id_list = g_slist_prepend(python_mainwin_scripts_id_list, GUINT_TO_POINTER(id));
338 prefs_toolbar_register_plugin_item(TOOLBAR_MAIN, "Python", entries[ii].label, mainwin_toolbar_callback, NULL);
345 /* this function doesn't really create menu items, but prepares a list that can be used
346 * in the compose create hook. It does however register the scripts for the toolbar editor */
347 static void create_compose_menus_and_items(GSList *filenames)
350 for(walk = filenames; walk; walk = walk->next) {
351 python_compose_scripts_names = g_slist_prepend(python_compose_scripts_names, g_strdup((gchar*)walk->data));
352 prefs_toolbar_register_plugin_item(TOOLBAR_COMPOSE, "Python", (gchar*)walk->data, compose_toolbar_callback, NULL);
356 static GtkActionEntry compose_tools_python_actions[] = {
357 {"Tools/PythonScripts", NULL, N_("Python scripts") },
360 static void ComposeActionData_destroy_cb(gpointer data)
362 ComposeActionData *dat = (ComposeActionData*)data;
367 static gboolean my_compose_create_hook(gpointer cw, gpointer data)
371 GtkActionEntry *entries;
372 GtkActionGroup *action_group;
373 Compose *compose = (Compose*)cw;
374 guint num_entries = g_slist_length(python_compose_scripts_names);
376 action_group = gtk_action_group_new("PythonPlugin");
377 gtk_action_group_add_actions(action_group, compose_tools_python_actions, 1, NULL);
378 entries = g_new0(GtkActionEntry, num_entries);
380 for(walk = python_compose_scripts_names; walk; walk = walk->next) {
381 ComposeActionData *dat;
383 entries[ii].name = walk->data;
384 entries[ii].label = walk->data;
385 entries[ii].callback = G_CALLBACK(python_compose_script_callback);
387 dat = g_new0(ComposeActionData, 1);
388 dat->name = g_strdup(walk->data);
389 dat->compose = compose;
391 gtk_action_group_add_actions_full(action_group, &(entries[ii]), 1, dat, ComposeActionData_destroy_cb);
394 gtk_ui_manager_insert_action_group(compose->ui_manager, action_group, 0);
396 MENUITEM_ADDUI_MANAGER(compose->ui_manager, "/Menu/Tools", "PythonScripts",
397 "Tools/PythonScripts", GTK_UI_MANAGER_MENU)
399 for(ii = 0; ii < num_entries; ii++) {
400 MENUITEM_ADDUI_MANAGER(compose->ui_manager, "/Menu/" PYTHON_SCRIPTS_ACTION_PREFIX, entries[ii].label,
401 entries[ii].name, GTK_UI_MANAGER_MENUITEM)
406 run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_COMPOSE, compose);
412 static void refresh_scripts_in_dir(const gchar *subdir, ToolbarType toolbar_type)
416 GError *error = NULL;
417 const char *filename;
418 GSList *filenames = NULL;
422 scripts_dir = g_strconcat(get_rc_dir(),
423 G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR,
424 G_DIR_SEPARATOR_S, subdir,
426 debug_print("Refreshing: %s\n", scripts_dir);
428 dir = g_dir_open(scripts_dir, 0, &error);
432 g_print("Could not open directory '%s': %s\n", subdir, error->message);
439 while((filename = g_dir_read_name(dir)) != NULL) {
442 fn = g_strdup(filename);
443 filenames = g_slist_prepend(filenames, fn);
448 if(toolbar_type == TOOLBAR_MAIN)
449 create_mainwindow_menus_and_items(filenames, num_entries);
450 else if(toolbar_type == TOOLBAR_COMPOSE)
451 create_compose_menus_and_items(filenames);
454 for(walk = filenames; walk; walk = walk->next)
456 g_slist_free(filenames);
459 static void browse_python_scripts_dir(GtkAction *action, gpointer data)
462 GdkAppLaunchContext *launch_context;
463 GError *error = NULL;
466 mainwin = mainwindow_get_mainwindow();
468 debug_print("Browse Python scripts: Problems getting the mainwindow\n");
471 launch_context = gdk_app_launch_context_new();
472 gdk_app_launch_context_set_screen(launch_context, gtk_widget_get_screen(mainwin->window));
473 uri = g_strconcat("file://", get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, NULL);
474 g_app_info_launch_default_for_uri(uri, G_APP_LAUNCH_CONTEXT(launch_context), &error);
477 debug_print("Could not open scripts dir browser: '%s'\n", error->message);
481 g_object_unref(launch_context);
485 static void refresh_python_scripts_menus(GtkAction *action, gpointer data)
487 remove_python_scripts_menus();
489 migrate_scripts_out_of_base_dir();
491 refresh_scripts_in_dir(PYTHON_SCRIPTS_MAIN_DIR, TOOLBAR_MAIN);
492 refresh_scripts_in_dir(PYTHON_SCRIPTS_COMPOSE_DIR, TOOLBAR_COMPOSE);
495 static GtkToggleActionEntry mainwindow_tools_python_toggle[] = {
496 {"Tools/ShowPythonConsole", NULL, N_("Show Python console..."),
497 NULL, NULL, G_CALLBACK(show_hide_python_console), FALSE},
500 static GtkActionEntry mainwindow_tools_python_actions[] = {
501 {"Tools/PythonScripts", NULL, N_("Python scripts") },
502 {"Tools/PythonScripts/Refresh", NULL, N_("Refresh"),
503 NULL, NULL, G_CALLBACK(refresh_python_scripts_menus) },
504 {"Tools/PythonScripts/Browse", NULL, N_("Browse"),
505 NULL, NULL, G_CALLBACK(browse_python_scripts_dir) },
506 {"Tools/PythonScripts/---", NULL, "---" },
509 void python_menu_init(void)
514 mainwin = mainwindow_get_mainwindow();
516 gtk_action_group_add_toggle_actions(mainwin->action_group, mainwindow_tools_python_toggle, 1, mainwin);
517 gtk_action_group_add_actions(mainwin->action_group, mainwindow_tools_python_actions, 3, mainwin);
519 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools", "ShowPythonConsole",
520 "Tools/ShowPythonConsole", GTK_UI_MANAGER_MENUITEM, id)
521 menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
523 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools", "PythonScripts",
524 "Tools/PythonScripts", GTK_UI_MANAGER_MENU, id)
525 menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
527 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Refresh",
528 "Tools/PythonScripts/Refresh", GTK_UI_MANAGER_MENUITEM, id)
529 menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
531 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Browse",
532 "Tools/PythonScripts/Browse", GTK_UI_MANAGER_MENUITEM, id)
533 menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
535 MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Separator1",
536 "Tools/PythonScripts/---", GTK_UI_MANAGER_SEPARATOR, id)
537 menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
539 refresh_python_scripts_menus(NULL, NULL);
542 void python_menu_done(void)
546 mainwin = mainwindow_get_mainwindow();
548 if(mainwin && !claws_is_exiting()) {
551 remove_python_scripts_menus();
553 for(walk = menu_id_list; walk; walk = walk->next)
554 gtk_ui_manager_remove_ui(mainwin->ui_manager, GPOINTER_TO_UINT(walk->data));
555 MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/ShowPythonConsole", 0);
556 MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts", 0);
557 MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/Refresh", 0);
558 MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/Browse", 0);
559 MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/---", 0);
563 gint plugin_init(gchar **error)
566 if(!check_plugin_version(MAKE_NUMERIC_VERSION(3,7,6,9), VERSION_NUMERIC, _("Python"), error))
570 hook_compose_create = hooks_register_hook(COMPOSE_CREATED_HOOKLIST, my_compose_create_hook, NULL);
571 if(hook_compose_create == (guint)-1) {
572 *error = g_strdup(_("Failed to register \"compose create hook\" in the Python plugin"));
576 /* script directories */
577 if(!make_sure_directories_exist(error))
580 /* initialize python interpreter */
583 /* initialize python interactive shell */
584 parasite_python_init();
586 /* initialize Claws Mail Python module */
587 claws_mail_python_init();
589 /* load menu options */
592 run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_STARTUP, NULL);
594 debug_print("Python plugin loaded\n");
599 gboolean plugin_done(void)
601 hooks_unregister_hook(COMPOSE_CREATED_HOOKLIST, hook_compose_create);
603 run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_SHUTDOWN, NULL);
608 gtk_widget_destroy(python_console);
609 python_console = NULL;
612 /* finialize python interpreter */
615 debug_print("Python plugin done and unloaded.\n");
619 const gchar *plugin_name(void)
624 const gchar *plugin_desc(void)
626 return _("This plugin provides Python integration features.\n"
627 "\nFor the most up-to-date API documentation, type\n"
628 "\n help(clawsmail)\n"
629 "\nin the interactive Python console under Tools -> Show Python console.\n"
630 "\nThe source distribution of this plugin comes with various example scripts "
631 "in the \"examples\" subdirectory. If you wrote a script that you would be "
632 "interested in sharing, feel free to send it to me to have it considered "
633 "for inclusion in the examples.\n"
634 "\nFeedback to <berndth@gmx.de> is welcome.");
637 const gchar *plugin_type(void)
642 const gchar *plugin_licence(void)
647 const gchar *plugin_version(void)
652 struct PluginFeature *plugin_provides(void)
654 static struct PluginFeature features[] =
655 { {PLUGIN_UTILITY, N_("Python integration")},
656 {PLUGIN_NOTHING, NULL}};