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