1 /*
2 * geanylua.c - This file is part of the Lua scripting plugin for the Geany IDE
3 * See the file "geanylua.c" for copyright information.
4 */
5
6 #include "glspi.h"
7
8
9 #define DIR_SEP G_DIR_SEPARATOR_S
10
11 #define USER_SCRIPT_FOLDER DIR_SEP "plugins" DIR_SEP "geanylua"
12
13 #define EVENTS_FOLDER USER_SCRIPT_FOLDER DIR_SEP "events" DIR_SEP
14
15 #define ON_SAVED_SCRIPT EVENTS_FOLDER "saved.lua"
16 #define ON_OPENED_SCRIPT EVENTS_FOLDER "opened.lua"
17 #define ON_CREATED_SCRIPT EVENTS_FOLDER "created.lua"
18 #define ON_ACTIVATED_SCRIPT EVENTS_FOLDER "activated.lua"
19
20 #define ON_PROJ_OPENED_SCRIPT EVENTS_FOLDER "proj-opened.lua"
21 #define ON_PROJ_SAVED_SCRIPT EVENTS_FOLDER "proj-saved.lua"
22 #define ON_PROJ_CLOSED_SCRIPT EVENTS_FOLDER "proj-closed.lua"
23
24 #define ON_INIT_SCRIPT EVENTS_FOLDER "init.lua"
25 #define ON_CLEANUP_SCRIPT EVENTS_FOLDER "cleanup.lua"
26 #define ON_CONFIGURE_SCRIPT EVENTS_FOLDER "configure.lua"
27
28 #define HOTKEYS_CFG DIR_SEP "hotkeys.cfg"
29 #define MAX_HOT_KEYS 100
30
31 PLUGIN_EXPORT
32 const gchar* glspi_version = VERSION;
33 PLUGIN_EXPORT
34 const guint glspi_abi = GEANY_ABI_VERSION;
35
36 GeanyData *glspi_geany_data=NULL;
37 GeanyPlugin *glspi_geany_plugin=NULL;
38
39 static struct {
40 GtkWidget *menu_item;
41 gchar *script_dir;
42 gchar *on_saved_script;
43 gchar *on_created_script;
44 gchar *on_opened_script;
45 gchar *on_activated_script;
46 gchar *on_init_script;
47 gchar *on_cleanup_script;
48 gchar *on_configure_script;
49 gchar *on_proj_opened_script;
50 gchar *on_proj_saved_script;
51 gchar *on_proj_closed_script;
52 GSList *script_list;
53 GtkAccelGroup *acc_grp;
54 GeanyKeyGroup *keybind_grp;
55 gchar **keybind_scripts;
56 } local_data;
57
58 #define SD local_data.script_dir
59 #define KG local_data.keybind_grp
60 #define KS local_data.keybind_scripts
61
62
63 /* Called by Geany, run a script associated with a keybinding. */
kb_activate(guint key_id)64 static void kb_activate(guint key_id)
65 {
66 if ((key_id<MAX_HOT_KEYS) && KS[key_id]) {
67 glspi_run_script(KS[key_id],0,NULL,SD);
68 }
69 }
70
71 /* Convert a script filename into a "pretty-printed" menu label. */
fixup_label(gchar * label)72 static gchar* fixup_label(gchar*label)
73 {
74 gint i;
75
76 if (isdigit(label[0])&&isdigit(label[1])&&('.'==label[2])&&(label[3])) {
77 memmove(label,label+3,strlen(label)-2);
78 }
79 /* Capitalize first char of menu label */
80 if (('_'==label[0])&&(label[1])) { /* Skip leading underscore */
81 label[1]=g_ascii_toupper(label[1]);
82 } else {
83 label[0]=g_ascii_toupper(label[0]);
84 }
85 /* Convert hyphens in filename to spaces for menu label */
86 for (i=0; label[i]; i++) { if ('-' == label[i]) {label[i]=' ';} }
87 return label;
88 }
89
90
91 /* Free all hotkey data */
hotkey_cleanup(void)92 static void hotkey_cleanup(void)
93 {
94 if (KS) { g_strfreev(KS); }
95 }
96
97
98 #define KEYFILE_FAIL(msg) \
99 if (geany->app->debug_mode) { \
100 g_printerr("%s: %s\n", PLUGIN_NAME, msg); \
101 } \
102 g_error_free(err); \
103 g_key_file_free(kf); \
104 kf=NULL;
105
106
107 /* Initialize the interface to Geany's keybindings API */
hotkey_init(void)108 static void hotkey_init(void)
109 {
110 gchar *hotkeys_cfg=g_strconcat(SD,HOTKEYS_CFG,NULL);
111 hotkey_cleanup(); /* Make sure we are in initial state. */
112 if (g_file_test(hotkeys_cfg,G_FILE_TEST_IS_REGULAR)) {
113 GError *err=NULL;
114 gchar*all=NULL;
115 gsize len;
116 if (g_file_get_contents(hotkeys_cfg,&all,&len,&err)) {
117 gchar**lines=g_strsplit(all, "\n", 0);
118 gint i;
119 gint n=0;
120 g_free(all);
121 for (i=0; lines[i]; i++) {
122 g_strstrip(lines[i]);
123 if ((lines[i][0]!='#')&&(lines[i][0]!='\0')) {
124 n++;
125 if (n==MAX_HOT_KEYS) { break; }
126 }
127 }
128 KS=g_new0(gchar*, n+1);
129 n=0;
130 for (i=0; lines[i]; i++) {
131 if ((lines[i][0]!='#')&&(lines[i][0]!='\0')) {
132 if (g_path_is_absolute(lines[i])) {
133 KS[n]=g_strdup(lines[i]);
134 } else {
135 KS[n]=g_build_filename(SD, lines[i], NULL);
136 }
137 n++;
138 if (n==MAX_HOT_KEYS) { break; }
139 }
140 }
141 g_strfreev(lines);
142 KG=plugin_set_key_group(glspi_geany_plugin, "lua_scripts", n, NULL);
143 for (i=0; i<n; i++) {
144 gchar *label=NULL;
145 gchar *name=NULL;
146 if (KS[i]) {
147 gchar*p=NULL;
148 label=g_path_get_basename(KS[i]);
149 fixup_label(label);
150 p=strchr(label,'_');
151 if (p) { *p=' ';}
152 p=strrchr(label, '.');
153 if (p && (g_ascii_strcasecmp(p, ".lua")==0)) {
154 *p='\0';
155 }
156 name=g_strdup_printf("lua_script_%d", i+1);
157 }
158 /* no default keycombos, just overridden by user settings */
159 keybindings_set_item(KG, i, kb_activate, 0, 0, name, label, NULL);
160 g_free(label);
161 g_free(name);
162 }
163 } else {
164 if (geany->app->debug_mode) {
165 g_printerr("%s: %s\n", PLUGIN_NAME, err->message);
166 }
167 g_error_free(err);
168 }
169 } else {
170 if (geany->app->debug_mode) {
171 g_printerr("%s: File not found %s\n", PLUGIN_NAME, hotkeys_cfg);
172 }
173 }
174 g_free(hotkeys_cfg);
175 }
176
177
178
179
on_doc_new(GObject * obj,GeanyDocument * doc,gpointer user_data)180 static void on_doc_new(GObject *obj, GeanyDocument *doc, gpointer user_data)
181 {
182 gint idx = doc->index;
183 if (g_file_test(local_data.on_created_script,G_FILE_TEST_IS_REGULAR)) {
184 glspi_run_script(local_data.on_created_script,idx+1, NULL, SD);
185 }
186 }
187
188
on_doc_save(GObject * obj,GeanyDocument * doc,gpointer user_data)189 static void on_doc_save(GObject *obj, GeanyDocument *doc, gpointer user_data)
190 {
191 gint idx = doc->index;
192 if (g_file_test(local_data.on_saved_script,G_FILE_TEST_IS_REGULAR)) {
193 glspi_run_script(local_data.on_saved_script,idx+1, NULL, SD);
194 }
195 }
196
197
198
on_doc_open(GObject * obj,GeanyDocument * doc,gpointer user_data)199 static void on_doc_open(GObject *obj, GeanyDocument *doc, gpointer user_data)
200 {
201 gint idx = doc->index;
202 if (g_file_test(local_data.on_opened_script,G_FILE_TEST_IS_REGULAR)) {
203 glspi_run_script(local_data.on_opened_script,idx+1, NULL, SD);
204 }
205 }
206
207
208
on_doc_activate(GObject * obj,GeanyDocument * doc,gpointer user_data)209 static void on_doc_activate(GObject *obj, GeanyDocument *doc, gpointer user_data)
210 {
211 gint idx = doc->index;
212 if (g_file_test(local_data.on_activated_script,G_FILE_TEST_IS_REGULAR)) {
213 glspi_run_script(local_data.on_activated_script,idx+1, NULL, SD);
214 }
215 }
216
217
218
on_proj_open(GObject * obj,GKeyFile * config,gpointer user_data)219 static void on_proj_open(GObject *obj, GKeyFile *config, gpointer user_data)
220 {
221 if (g_file_test(local_data.on_proj_opened_script,G_FILE_TEST_IS_REGULAR)) {
222 glspi_run_script(local_data.on_proj_opened_script,0,config, SD);
223 }
224 }
225
226
227
on_proj_save(GObject * obj,GKeyFile * config,gpointer user_data)228 static void on_proj_save(GObject *obj, GKeyFile *config, gpointer user_data)
229 {
230 if (g_file_test(local_data.on_proj_saved_script,G_FILE_TEST_IS_REGULAR)) {
231 glspi_run_script(local_data.on_proj_saved_script,0,config, SD);
232 }
233 }
234
235
236
on_proj_close(GObject * obj,gpointer user_data)237 static void on_proj_close(GObject *obj, gpointer user_data)
238 {
239 if (g_file_test(local_data.on_proj_closed_script,G_FILE_TEST_IS_REGULAR)) {
240 glspi_run_script(local_data.on_proj_closed_script,0, NULL, SD);
241 }
242 }
243
244
245 PLUGIN_EXPORT
246 PluginCallback glspi_geany_callbacks[] = {
247 {"document-new", (GCallback) &on_doc_new, TRUE, NULL},
248 {"document-open", (GCallback) &on_doc_open, TRUE, NULL},
249 {"document-save", (GCallback) &on_doc_save, TRUE, NULL},
250 {"document-activate", (GCallback) &on_doc_activate, TRUE, NULL},
251 {"project-open", (GCallback) &on_proj_open, TRUE, NULL},
252 {"project-save", (GCallback) &on_proj_save, TRUE, NULL},
253 {"project-close", (GCallback) &on_proj_close, TRUE, NULL},
254 {NULL, NULL, FALSE, NULL}
255 };
256
257
258
259 /* Callback when the menu item is clicked */
menu_item_activate(GtkMenuItem * menuitem,gpointer gdata)260 static void menu_item_activate(GtkMenuItem * menuitem, gpointer gdata)
261 {
262 glspi_run_script(gdata, 0,NULL, SD);
263 }
264
265
266 #define is_blank(c) ( (c==32) || (c==9) )
267
268
269 /*
270 Check if the script file begins with a special comment in the form:
271 -- @ACCEL@ <Modifiers>key
272 If we find one, parse it, and bind that key combo to its menu item.
273 See gtk_accelerator_parse() doc for more info on accel syntax...
274 */
assign_accel(GtkWidget * w,char * fn)275 static void assign_accel(GtkWidget*w, char*fn)
276 {
277 FILE*f=fopen(fn,"r");
278 gchar buf[512];
279 gint len;
280 if (!f) { return; }
281 len=fread(buf,1,sizeof(buf)-1,f);
282 if (len>0) {
283 gchar*p1=buf;
284 buf[len]='\0';
285 while (*p1 && is_blank(*p1)) p1++;
286 if ( strncmp(p1,"--", 2) == 0 ) {
287 p1+=2;
288 while (*p1 && is_blank(*p1)) p1++;
289 if ( strncmp(p1,"@ACCEL@", 7) == 0 ) {
290 guint key=0;
291 GdkModifierType mods=0;
292 p1+=7;
293 while (*p1 && is_blank(*p1)) p1++;
294 if (*p1) {
295 gchar*p2=p1;
296 while ( (*p2) && (!isspace(*p2)) ) { p2++; }
297 *p2='\0';
298 gtk_accelerator_parse(p1, &key, &mods);
299 if ( key && mods ) {
300 if (!local_data.acc_grp) {
301 local_data.acc_grp=gtk_accel_group_new();
302 }
303 gtk_widget_add_accelerator(w,
304 "activate",local_data.acc_grp,key,mods,GTK_ACCEL_VISIBLE);
305 }
306 }
307 }
308 }
309 }
310 fclose(f);
311 }
312
313
314
315
316 static GtkWidget* new_menu(GtkWidget *parent, const gchar* script_dir, const gchar*title);
317
318 /* GSList "for each" callback to create a menu item for each found script */
init_menu(gpointer data,gpointer user_data)319 static void init_menu(gpointer data, gpointer user_data)
320 {
321 g_return_if_fail(data && user_data);
322 if (g_file_test(data,G_FILE_TEST_IS_REGULAR)) {
323 gchar *dot = strrchr(data, '.');
324 if ( dot && (((gpointer)dot)>data) && (g_ascii_strcasecmp(dot, ".lua")==0) ) {
325 GtkWidget *item;
326 gchar*label=strrchr(data,DIR_SEP[0]);
327 gchar *tmp=NULL;
328 if (label) { label++; } else { label=data; }
329 tmp=g_malloc0(strlen(label));
330 strncpy(tmp, label, dot-label);
331 label=tmp;
332 label=fixup_label(label);
333 if ('_'==*(dot-1)) { strcpy(strchr(label, '\0')-1, "..."); }
334 item = gtk_menu_item_new_with_mnemonic(label);
335 g_free(label);
336 gtk_container_add(GTK_CONTAINER(user_data), item);
337 g_signal_connect(G_OBJECT(item), "activate",
338 G_CALLBACK(menu_item_activate), data);
339 assign_accel(item, data);
340 }
341 } else {
342 if (g_file_test(data,G_FILE_TEST_IS_DIR)) {
343 gchar*label=strrchr(data,DIR_SEP[0]);
344 if (label) { label++; } else { label=data; }
345 if ((g_ascii_strcasecmp(label,"events")!=0)&&(g_ascii_strcasecmp(label,"support")!=0)) {
346 label=g_strdup(label);
347 fixup_label(label);
348 new_menu(user_data, data, label); /* Recursive */
349 g_free(label);
350 }
351 }
352 }
353 }
354
355
356
new_menu(GtkWidget * parent,const gchar * script_dir,const gchar * title)357 static GtkWidget* new_menu(GtkWidget *parent, const gchar* script_dir, const gchar*title)
358 {
359 GSList *script_names=utils_get_file_list_full(script_dir, TRUE, TRUE, NULL);
360 if (script_names) {
361 GtkWidget *menu = gtk_menu_new();
362 GtkWidget *menu_item = gtk_menu_item_new_with_mnemonic(title);
363 g_slist_foreach(script_names, init_menu, menu);
364 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), menu);
365 gtk_container_add(GTK_CONTAINER(parent), menu_item);
366 gtk_widget_show_all(menu_item);
367 local_data.script_list=g_slist_concat(local_data.script_list,script_names);
368 return menu_item;
369 }
370 g_printerr("%s: No scripts found in %s\n", PLUGIN_NAME, script_dir);
371 return NULL;
372 }
373
374
375
build_menu(void)376 static void build_menu(void)
377 {
378 local_data.script_list = NULL;
379 local_data.acc_grp=NULL;
380 local_data.menu_item=new_menu(main_widgets->tools_menu,
381 local_data.script_dir, _("_Lua Scripts"));
382 if (local_data.acc_grp) {
383 gtk_window_add_accel_group(GTK_WINDOW(main_widgets->window), local_data.acc_grp);
384 }
385 }
386
387
get_data_dir(void)388 static gchar *get_data_dir(void)
389 {
390 #ifdef G_OS_WIN32
391 gchar *install_dir, *result;
392 # if GLIB_CHECK_VERSION(2, 16, 0)
393 install_dir = g_win32_get_package_installation_directory_of_module(NULL);
394 # else
395 install_dir = g_win32_get_package_installation_directory(NULL, NULL);
396 # endif
397 result = g_strconcat(install_dir, "\\share", NULL);
398 g_free(install_dir);
399 return result;
400 #else
401 return g_strdup(GEANYPLUGINS_DATADIR);
402 #endif
403 }
404
405
406
407 /* Called by Geany to initialize the plugin */
408 PLUGIN_EXPORT
glspi_init(GeanyData * data,GeanyPlugin * plugin)409 void glspi_init (GeanyData *data, GeanyPlugin *plugin)
410 {
411 glspi_geany_data = data;
412 glspi_geany_plugin = plugin;
413
414 local_data.script_dir =
415 g_strconcat(geany->app->configdir, USER_SCRIPT_FOLDER, NULL);
416
417 if (!g_file_test(local_data.script_dir, G_FILE_TEST_IS_DIR)) {
418 gchar *datadir = get_data_dir();
419 g_free(local_data.script_dir);
420 local_data.script_dir =
421 g_build_path(G_DIR_SEPARATOR_S, datadir, "geany-plugins", "geanylua", NULL);
422 g_free(datadir);
423 }
424 if (geany->app->debug_mode) {
425 g_printerr(_(" ==>> %s: Building menu from '%s'\n"),
426 PLUGIN_NAME, local_data.script_dir);
427 }
428 local_data.on_saved_script =
429 g_strconcat(geany->app->configdir, ON_SAVED_SCRIPT, NULL);
430 local_data.on_opened_script =
431 g_strconcat(geany->app->configdir, ON_OPENED_SCRIPT, NULL);
432 local_data.on_created_script =
433 g_strconcat(geany->app->configdir, ON_CREATED_SCRIPT, NULL);
434 local_data.on_activated_script =
435 g_strconcat(geany->app->configdir, ON_ACTIVATED_SCRIPT, NULL);
436 local_data.on_init_script =
437 g_strconcat(geany->app->configdir, ON_INIT_SCRIPT, NULL);
438 local_data.on_cleanup_script =
439 g_strconcat(geany->app->configdir, ON_CLEANUP_SCRIPT, NULL);
440 local_data.on_configure_script =
441 g_strconcat(geany->app->configdir, ON_CONFIGURE_SCRIPT, NULL);
442 local_data.on_proj_opened_script =
443 g_strconcat(geany->app->configdir, ON_PROJ_OPENED_SCRIPT, NULL);
444 local_data.on_proj_saved_script =
445 g_strconcat(geany->app->configdir, ON_PROJ_SAVED_SCRIPT, NULL);
446 local_data.on_proj_closed_script =
447 g_strconcat(geany->app->configdir, ON_PROJ_CLOSED_SCRIPT, NULL);
448
449 glspi_set_sci_cmd_hash(TRUE);
450 glspi_set_key_cmd_hash(TRUE);
451 build_menu();
452 hotkey_init();
453 if (g_file_test(local_data.on_init_script,G_FILE_TEST_IS_REGULAR)) {
454 glspi_run_script(local_data.on_init_script,0,NULL, SD);
455 }
456 }
457
458
459
460 /* GSList "for each" callback to free our script list items */
free_script_names(gpointer data,gpointer user_data)461 static void free_script_names(gpointer data, gpointer user_data)
462 {
463 if (data) { g_free(data); }
464 }
465
466
remove_menu(void)467 static void remove_menu(void)
468 {
469 if (local_data.acc_grp) { g_object_unref(local_data.acc_grp); }
470 if (local_data.menu_item) { gtk_widget_destroy(local_data.menu_item); }
471 }
472
473
474 #define done(f) if (local_data.f) g_free(local_data.f)
475
476 /* Called by Geany when it is time to free the plugin's resources */
477 PLUGIN_EXPORT
glspi_cleanup(void)478 void glspi_cleanup(void)
479 {
480
481 if (g_file_test(local_data.on_cleanup_script,G_FILE_TEST_IS_REGULAR)) {
482 glspi_run_script(local_data.on_cleanup_script,0,NULL, SD);
483 }
484 remove_menu();
485 hotkey_cleanup();
486 done(script_dir);
487 done(on_saved_script);
488 done(on_created_script);
489 done(on_opened_script);
490 done(on_activated_script);
491 done(on_init_script);
492 done(on_cleanup_script);
493 done(on_configure_script);
494 done(on_proj_opened_script);
495 done(on_proj_saved_script);
496 done(on_proj_closed_script);
497
498 if (local_data.script_list) {
499 g_slist_foreach(local_data.script_list, free_script_names, NULL);
500 g_slist_free(local_data.script_list);
501 }
502 glspi_set_sci_cmd_hash(FALSE);
503 glspi_set_key_cmd_hash(FALSE);
504
505 }
506
507
508
509
510
511 /*
512 Called by geany when user clicks preferences button
513 in plugin manager dialog.
514 */
515 PLUGIN_EXPORT
glspi_configure(GtkWidget * parent)516 void glspi_configure(GtkWidget *parent)
517 {
518 if (g_file_test(local_data.on_configure_script,G_FILE_TEST_IS_REGULAR)) {
519 glspi_run_script(local_data.on_configure_script,0,NULL, SD);
520 } else {
521 gint flags=GTK_DIALOG_DESTROY_WITH_PARENT|GTK_DIALOG_MODAL;
522 gint type=GTK_MESSAGE_INFO;
523 GtkWidget *dlg=gtk_message_dialog_new(
524 GTK_WINDOW(parent),flags,type,GTK_BUTTONS_OK,_("Nothing to configure!"));
525 gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg),
526 _("You can create the script:\n\n\"%s\"\n\n"
527 "to add your own custom configuration dialog."),
528 local_data.on_configure_script);
529 gtk_window_set_title(GTK_WINDOW(dlg),PLUGIN_NAME);
530 gtk_dialog_run(GTK_DIALOG(dlg));
531 gtk_widget_destroy(dlg);
532 }
533 }
534
glspi_rescan(lua_State * L)535 static gint glspi_rescan(lua_State* L) {
536 remove_menu();
537 build_menu();
538 hotkey_init();
539 return 0;
540 }
541
542 static const struct luaL_reg glspi_mnu_funcs[] = {
543 {"rescan", glspi_rescan},
544 {NULL,NULL}
545 };
546
547
glspi_init_mnu_funcs(lua_State * L)548 void glspi_init_mnu_funcs(lua_State *L) {
549 luaL_register(L, NULL,glspi_mnu_funcs);
550 }
551