Coder
/* $Id$ */
							static char const _copyright[] =
							"Copyright © 2011-2020 Pierre Pronchery <khorben@defora.org>";
							/* This file is part of DeforaOS Desktop Coder */
							static char const _license[] =
							"This program is free software: you can redistribute it and/or modify\n"
							"it under the terms of the GNU General Public License as published by\n"
							"the Free Software Foundation, version 3 of the License.\n"
							"\n"
							"This program is distributed in the hope that it will be useful,\n"
							"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
							"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
							"GNU General Public License for more details.\n"
							"\n"
							"You should have received a copy of the GNU General Public License\n"
							"along with this program. If not, see <http://www.gnu.org/licenses/>.";
							/* TODO:
							 * - add a "backend" type of plug-ins (asm, hexedit, make, project, UWff...)
							 * - add a "plug-in" type of plug-ins (time tracker, ...) */
							#include <stdlib.h>
							#include <string.h>
							#include <errno.h>
							#include <libintl.h>
							#include <gdk/gdkkeysyms.h>
							#include <Desktop.h>
							#include "callbacks.h"
							#include "coder.h"
							#include "../config.h"
							#define _(string) gettext(string)
							#define N_(string) (string)
							#ifndef PREFIX
							# define PREFIX		"/usr/local"
							#endif
							#ifndef PROGNAME_HELPER
							# define PROGNAME_HELPER	"helper"
							#endif
							/* Coder */
							/* private */
							/* types */
							struct _Coder
							{
								Config * config;
								Project ** projects;
								size_t projects_cnt;
								Project * cur;
								/* widgets */
								/* toolbar */
								GtkWidget * tb_window;
								/* preferences */
								GtkWidget * pr_window;
								GtkWidget * pr_editor_command;
								GtkWidget * pr_editor_terminal;
								/* files */
								GtkWidget * fi_window;
								GtkWidget * fi_combo;
								GtkWidget * fi_view;
								/* about */
								GtkWidget * ab_window;
							};
							/* constants */
							#define ICON_NAME	"applications-development"
							static char const * _authors[] =
							{
								"Pierre Pronchery <khorben@defora.org>",
								NULL
							};
							/* menubar */
							static const DesktopMenu _coder_menu_file[] =
							{
								{ N_("_New file..."), G_CALLBACK(on_file_new), GTK_STOCK_NEW,
									GDK_CONTROL_MASK, GDK_KEY_N },
								{ N_("_Open file..."), G_CALLBACK(on_file_open), GTK_STOCK_OPEN,
									GDK_CONTROL_MASK, GDK_KEY_O },
								{ "", NULL , NULL, 0, 0 },
								{ N_("_Preferences..."), G_CALLBACK(on_file_preferences),
									GTK_STOCK_PREFERENCES, GDK_CONTROL_MASK, GDK_KEY_P },
								{ "", NULL, NULL, 0, 0 },
								{ N_("_Exit"), G_CALLBACK(on_file_exit), GTK_STOCK_QUIT,
									GDK_CONTROL_MASK, GDK_KEY_Q },
								{ NULL, NULL, NULL, 0, 0 }
							};
							/* FIXME will certainly be dynamic */
							static const DesktopMenu _coder_menu_project[] =
							{
								{ N_("_New project..."), G_CALLBACK(on_project_new), GTK_STOCK_NEW, 0,
									0 },
								{ N_("_Open project..."), G_CALLBACK(on_project_open), GTK_STOCK_OPEN,
									0, 0 },
								{ N_("_Save project"), G_CALLBACK(on_project_save), GTK_STOCK_SAVE,
									GDK_CONTROL_MASK, GDK_KEY_S },
								{ N_("Save project _As..."), G_CALLBACK(on_project_save_as),
									GTK_STOCK_SAVE_AS, 0, 0 },
								{ "", NULL, NULL, 0, 0 },
								{ N_("_Properties..."), G_CALLBACK(on_project_properties),
									GTK_STOCK_PROPERTIES, GDK_MOD1_MASK, GDK_KEY_Return },
								{ NULL, NULL, NULL, 0, 0 }
							};
							static const DesktopMenu _coder_menu_view[] =
							{
								{ N_("_Files"), G_CALLBACK(on_view_files), NULL, 0, 0 },
								{ NULL, NULL, NULL, 0, 0 }
							};
							static const DesktopMenu _coder_menu_tools[] =
							{
								{ N_("_Debugger"), G_CALLBACK(on_tools_debugger), NULL, 0, 0 },
								{ N_("_PHP console"), G_CALLBACK(on_tools_php_console), NULL, 0, 0 },
								{ N_("_Simulator"), G_CALLBACK(on_tools_simulator), "stock_cell-phone",
									0, 0 },
								{ N_("S_QL console"), G_CALLBACK(on_tools_sql_console),
									"stock_insert-table", 0, 0 },
								{ NULL, NULL, NULL, 0, 0 }
							};
							static const DesktopMenu _coder_menu_help[] =
							{
								{ N_("API _Reference"), G_CALLBACK(on_help_api_reference),
									"help-contents", 0, 0 },
								{ N_("_Contents"), G_CALLBACK(on_help_contents), "help-contents", 0,
									GDK_KEY_F1 },
								{ N_("_About"), G_CALLBACK(on_help_about), GTK_STOCK_ABOUT, 0, 0 },
								{ NULL, NULL, NULL, 0, 0 }
							};
							static const DesktopMenubar _coder_menubar[] =
							{
								{ N_("_File"),		_coder_menu_file },
								{ N_("_Project"),	_coder_menu_project },
								{ N_("_View"),		_coder_menu_view },
								{ N_("_Tools"),		_coder_menu_tools },
								{ N_("_Help"),		_coder_menu_help },
								{ NULL,			NULL }
							};
							/* variables */
							/* toolbar */
							static DesktopToolbar _coder_toolbar[] =
							{
								{ N_("Exit"), G_CALLBACK(on_file_exit), GTK_STOCK_QUIT, 0, 0, NULL },
								{ NULL, NULL, NULL, 0, 0, NULL }
							};
							/* prototypes */
							static Project * _coder_get_current_project(Coder * coder);
							/* public */
							/* functions */
							/* coder_new */
							static void _new_config(Coder * g);
							Coder * coder_new(void)
							{
								Coder * coder;
								GtkAccelGroup * group;
								GtkWidget * vbox;
								GtkWidget * hbox;
								GtkWidget * widget;
								if((coder = malloc(sizeof(*coder))) == NULL)
									return NULL;
								coder->projects = NULL;
								coder->projects_cnt = 0;
								coder->cur = NULL;
								_new_config(coder);
								/* main window */
								group = gtk_accel_group_new();
								coder->tb_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
								gtk_window_add_accel_group(GTK_WINDOW(coder->tb_window), group);
							#if GTK_CHECK_VERSION(2, 6, 0)
								gtk_window_set_icon_name(GTK_WINDOW(coder->tb_window), ICON_NAME);
							#endif
								gtk_window_set_title(GTK_WINDOW(coder->tb_window), _("Coder"));
								gtk_window_set_resizable(GTK_WINDOW(coder->tb_window), FALSE);
								g_signal_connect_swapped(coder->tb_window, "delete-event", G_CALLBACK(
											on_closex), coder);
							#if GTK_CHECK_VERSION(3, 0, 0)
								vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
							#else
								vbox = gtk_vbox_new(FALSE, 0);
							#endif
								/* menubar */
								widget = desktop_menubar_create(_coder_menubar, coder, group);
								gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
								/* toolbar */
								widget = desktop_toolbar_create(_coder_toolbar, coder, group);
								g_object_unref(group);
								gtk_box_pack_start(GTK_BOX(vbox), widget, FALSE, TRUE, 0);
								gtk_container_add(GTK_CONTAINER(coder->tb_window), vbox);
								/* files */
								coder->fi_window = NULL;
								coder_show_files(coder, TRUE);
								/* about */
								coder->ab_window = NULL;
								gtk_widget_show_all(coder->tb_window);
								return coder;
							}
							static char * _config_file(void);
							static void _new_config(Coder * coder)
							{
								char * filename;
								if((coder->config = config_new()) == NULL)
								{
									coder_error(coder, strerror(errno), 0);
									return;
								}
								config_load(coder->config, PREFIX "/etc/" PACKAGE ".conf");
								if((filename = _config_file()) == NULL)
									return;
								config_load(coder->config, filename);
								free(filename);
							}
							static char * _config_file(void)
							{
								char const conffile[] = ".coder";
								char const * homedir;
								size_t len;
								char * filename;
								if((homedir = getenv("HOME")) == NULL)
									return NULL;
								len = strlen(homedir) + 1 + strlen(conffile) + 1;
								if((filename = malloc(len)) == NULL)
									return NULL;
								snprintf(filename, len, "%s/%s", homedir, conffile);
								return filename;
							}
							/* coder_delete */
							void coder_delete(Coder * coder)
							{
								char * filename;
								size_t i;
								if((filename = _config_file()) != NULL)
								{
									config_save(coder->config, filename);
									free(filename);
								}
								config_delete(coder->config);
								for(i = 0; i < coder->projects_cnt; i++)
									project_delete(coder->projects[i]);
								free(coder->projects);
								free(coder);
							}
							/* useful */
							/* coder_about */
							static gboolean _about_on_closex(gpointer data);
							void coder_about(Coder * coder)
							{
								if(coder->ab_window != NULL)
								{
									gtk_window_present(GTK_WINDOW(coder->ab_window));
									return;
								}
								coder->ab_window = desktop_about_dialog_new();
								gtk_window_set_transient_for(GTK_WINDOW(coder->ab_window), GTK_WINDOW(
											coder->tb_window));
								desktop_about_dialog_set_authors(coder->ab_window, _authors);
								desktop_about_dialog_set_comments(coder->ab_window,
										_("Integrated Development Environment for the DeforaOS"
										" desktop"));
								desktop_about_dialog_set_copyright(coder->ab_window, _copyright);
								desktop_about_dialog_set_logo_icon_name(coder->ab_window, ICON_NAME);
								desktop_about_dialog_set_license(coder->ab_window, _license);
								desktop_about_dialog_set_name(coder->ab_window, PACKAGE);
								desktop_about_dialog_set_version(coder->ab_window, VERSION);
								desktop_about_dialog_set_website(coder->ab_window,
										"https://www.defora.org/");
								g_signal_connect_swapped(coder->ab_window, "delete-event", G_CALLBACK(
											_about_on_closex), coder);
								gtk_widget_show(coder->ab_window);
							}
							static gboolean _about_on_closex(gpointer data)
							{
								Coder * coder = data;
								gtk_widget_hide(coder->ab_window);
								return TRUE;
							}
							/* coder_api_reference */
							int coder_api_reference(Coder * coder)
							{
								char const * argv[] = { PROGNAME_HELPER, NULL };
								const unsigned int flags = G_SPAWN_SEARCH_PATH;
								GError * error = NULL;
								if(g_spawn_async(NULL, argv, NULL, flags, NULL, NULL, NULL, &error)
										== FALSE)
								{
									coder_error(coder, error->message, 1);
									g_error_free(error);
									return -1;
								}
								return 0;
							}
							/* coder_error */
							int coder_error(Coder * coder, char const * message, int ret)
							{
								GtkWidget * dialog;
								dialog = gtk_message_dialog_new(GTK_WINDOW(coder->tb_window),
										GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
										GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
							#if GTK_CHECK_VERSION(2, 6, 0)
										"%s", _("Error"));
								gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
							#endif
										"%s", message);
								gtk_window_set_title(GTK_WINDOW(dialog), _("Error"));
								gtk_dialog_run(GTK_DIALOG(dialog));
								gtk_widget_destroy(dialog);
								return ret;
							}
							/* coder_file_open */
							void coder_file_open(Coder * coder, char const * filename)
							{
								/* FIXME really use the MIME sub-system */
								char * argv[] = { NULL, NULL, NULL };
								char const * p;
								GError * error = NULL;
								if((p = config_get(coder->config, "editor", "command")) == NULL)
									p = "editor"; /* XXX gather defaults in a common place */
								if((argv[0] = strdup(p)) == NULL)
									return; /* XXX report error */
								if(filename != NULL)
									argv[1] = strdup(filename); /* XXX check and report error */
								if(g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
											NULL, &error) != TRUE)
								{
									coder_error(coder, error->message, 1);
									g_error_free(error);
								}
								free(argv[1]);
								free(argv[0]);
							}
							/* coder_project_open */
							int coder_project_open(Coder * coder, char const * filename)
							{
								Project * project;
								if((project = project_new()) == NULL)
									return -coder_error(coder, error_get(NULL), 1);
								if(project_load(project, filename) != 0
										|| coder_project_open_project(coder, project) != 0)
								{
									project_delete(project);
									return -coder_error(coder, error_get(NULL), 1);
								}
								coder->cur = project;
							#if GTK_CHECK_VERSION(2, 24, 0)
								gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(coder->fi_combo),
										project_get_package(project));
							#else
								gtk_combo_box_append_text(GTK_COMBO_BOX(coder->fi_combo),
										project_get_package(project));
							#endif
								/* FIXME doesn't always select the last project opened */
								gtk_combo_box_set_active(GTK_COMBO_BOX(coder->fi_combo),
										gtk_combo_box_get_active(GTK_COMBO_BOX(coder->fi_combo))
										+ 1);
								return 0;
							}
							/* coder_project_open_dialog */
							void coder_project_open_dialog(Coder * coder)
							{
								GtkWidget * dialog;
								GtkFileFilter * filter;
								gchar * filename = NULL;
								dialog = gtk_file_chooser_dialog_new(_("Open project..."),
										GTK_WINDOW(coder->tb_window),
										GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL,
										GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN,
										GTK_RESPONSE_ACCEPT, NULL);
								filter = gtk_file_filter_new();
								gtk_file_filter_set_name(filter, _("Project files"));
								gtk_file_filter_add_pattern(filter, "project.conf");
								gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
								gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(dialog), filter);
								filter = gtk_file_filter_new();
								gtk_file_filter_set_name(filter, _("All files"));
								gtk_file_filter_add_pattern(filter, "*");
								gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
								if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
									filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(
												dialog));
								gtk_widget_destroy(dialog);
								if(filename == NULL)
									return;
								coder_project_open(coder, filename);
								g_free(filename);
							}
							/* coder_project_open_project */
							int coder_project_open_project(Coder * coder, Project * project)
							{
								Project ** p;
								if(project == NULL)
									return -error_set_code(1, "%s", strerror(EINVAL));;
								if((p = realloc(coder->projects, sizeof(*p) * (coder->projects_cnt + 1)))
										== NULL)
									return -error_set_code(1, "%s", strerror(errno));
								coder->projects = p;
								coder->projects[coder->projects_cnt++] = project;
								return 0;
							}
							/* coder_project_properties */
							void coder_project_properties(Coder * coder)
							{
								Project * project;
								if((project = _coder_get_current_project(coder)) == NULL)
									return;
								project_properties(project);
							}
							/* coder_project_save */
							int coder_project_save(Coder * coder)
							{
								Project * project;
								if((project = _coder_get_current_project(coder)) == NULL)
									return -1;
								if(project_get_pathname(project) == NULL)
									return coder_project_save_dialog(coder);
								return project_save(project);
							}
							/* coder_project_save_as */
							int coder_project_save_as(Coder * coder, char const * filename)
							{
								Project * project;
								if((project = _coder_get_current_project(coder)) == NULL)
									return -1;
								if(project_set_pathname(project, filename) != 0)
									return -1;
								return project_save(project);
							}
							/* coder_project_save_dialog */
							int coder_project_save_dialog(Coder * coder)
							{
								int ret = -1;
								Project * project;
								GtkWidget * dialog;
								gchar * filename = NULL;
								if((project = _coder_get_current_project(coder)) == NULL)
									return -1;
								dialog = gtk_file_chooser_dialog_new(_("Save project as..."), NULL,
										GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL,
										GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN,
										GTK_RESPONSE_ACCEPT, NULL);
								/* FIXME add options? (recursive save) */
								if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT)
									filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(
												dialog));
								gtk_widget_destroy(dialog);
								if(filename != NULL)
									ret = coder_project_save_as(coder, filename);
								g_free(filename);
								return ret;
							}
							/* coder_show_files */
							static void _show_files_window(Coder * coder);
							/* callbacks */
							static gboolean _files_on_closex(gpointer data);
							void coder_show_files(Coder * coder, gboolean show)
							{
								if(coder->fi_window == NULL)
									_show_files_window(coder);
								if(show)
									gtk_window_present(GTK_WINDOW(coder->fi_window));
								else
									gtk_widget_hide(coder->fi_window);
							}
							static void _show_files_window(Coder * coder)
							{
								GtkWidget * vbox;
								GtkWidget * hbox;
								coder->fi_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
								gtk_window_set_default_size(GTK_WINDOW(coder->fi_window), 150, 200);
								gtk_window_set_title(GTK_WINDOW(coder->fi_window), _("Files"));
								g_signal_connect_swapped(coder->fi_window, "delete-event", G_CALLBACK(
											_files_on_closex), coder);
							#if GTK_CHECK_VERSION(3, 0, 0)
								hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
								vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
							#else
								hbox = gtk_hbox_new(FALSE, 0);
								vbox = gtk_vbox_new(FALSE, 0);
							#endif
								/* FIXME use gtk_container_set_border_width() instead */
								gtk_box_pack_start(GTK_BOX(hbox), vbox, TRUE, TRUE, 2);
							#if GTK_CHECK_VERSION(2, 24, 0)
								coder->fi_combo = gtk_combo_box_text_new();
							#else
								coder->fi_combo = gtk_combo_box_new_text();
							#endif
								gtk_box_pack_start(GTK_BOX(vbox), coder->fi_combo, FALSE, TRUE, 2);
								coder->fi_view = gtk_tree_view_new();
								gtk_box_pack_start(GTK_BOX(vbox), coder->fi_view, TRUE, TRUE, 2);
								gtk_container_add(GTK_CONTAINER(coder->fi_window), hbox);
								gtk_widget_show_all(hbox);
							}
							/* callbacks */
							static gboolean _files_on_closex(gpointer data)
							{
								Coder * coder = data;
								gtk_widget_hide(coder->fi_window);
								return TRUE;
							}
							/* coder_show_preferences */
							static void _show_preferences_window(Coder * coder);
							static void _preferences_set(Coder * coder);
							/* callbacks */
							static gboolean _on_preferences_closex(gpointer data);
							static void _on_preferences_apply(gpointer data);
							static void _on_preferences_cancel(gpointer data);
							static void _on_preferences_ok(gpointer data);
							void coder_show_preferences(Coder * coder, gboolean show)
							{
								if(coder->pr_window == NULL)
									_show_preferences_window(coder);
								if(show)
									gtk_window_present(GTK_WINDOW(coder->pr_window));
								else
									gtk_widget_hide(coder->pr_window);
							}
							static void _show_preferences_window(Coder * coder)
							{
								GtkWidget * vbox;
								GtkWidget * nb;
								GtkWidget * nb_vbox;
								GtkWidget * hbox;
								GtkWidget * b_ok;
								GtkWidget * b_apply;
								GtkWidget * b_cancel;
								coder->pr_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); /* XXX dialog */
								gtk_container_set_border_width(GTK_CONTAINER(coder->pr_window), 4);
								gtk_window_set_title(GTK_WINDOW(coder->pr_window), _("Preferences"));
								g_signal_connect_swapped(coder->pr_window, "delete-event", G_CALLBACK(
											_on_preferences_closex), coder);
								vbox = gtk_vbox_new(FALSE, 4);
								nb = gtk_notebook_new();
								/* notebook page editor */
								nb_vbox = gtk_vbox_new(FALSE, 4);
								gtk_container_set_border_width(GTK_CONTAINER(nb_vbox), 4);
							#if GTK_CHECK_VERSION(3, 0, 0)
								hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
							#else
								hbox = gtk_hbox_new(FALSE, 4);
							#endif
								gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Editor:")), FALSE,
										TRUE, 0);
								coder->pr_editor_command = gtk_entry_new();
								gtk_box_pack_start(GTK_BOX(hbox), coder->pr_editor_command, TRUE, TRUE,
										0);
								gtk_box_pack_start(GTK_BOX(nb_vbox), hbox, FALSE, TRUE, 0);
								coder->pr_editor_terminal = gtk_check_button_new_with_mnemonic(
										_("Run in a _terminal"));
								gtk_box_pack_start(GTK_BOX(nb_vbox), coder->pr_editor_terminal, FALSE,
										TRUE, 0);
								gtk_notebook_append_page(GTK_NOTEBOOK(nb), nb_vbox, gtk_label_new(
											_("Editor")));
								/* notebook page plug-ins */
								nb_vbox = gtk_vbox_new(FALSE, 0);
								gtk_container_set_border_width(GTK_CONTAINER(nb_vbox), 4);
								gtk_notebook_append_page(GTK_NOTEBOOK(nb), nb_vbox, gtk_label_new(
											_("Plug-ins")));
								gtk_box_pack_start(GTK_BOX(vbox), nb, TRUE, TRUE, 0);
								/* buttons */
							#if GTK_CHECK_VERSION(3, 0, 0)
								hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
							#else
								hbox = gtk_hbox_new(TRUE, 4);
							#endif
								b_ok = gtk_button_new_from_stock(GTK_STOCK_OK);
								g_signal_connect_swapped(b_ok, "clicked", G_CALLBACK(
											_on_preferences_ok), coder);
								gtk_box_pack_end(GTK_BOX(hbox), b_ok, FALSE, TRUE, 0);
								b_apply = gtk_button_new_from_stock(GTK_STOCK_APPLY);
								g_signal_connect_swapped(b_apply, "clicked", G_CALLBACK(
											_on_preferences_apply), coder);
								gtk_box_pack_end(GTK_BOX(hbox), b_apply, FALSE, TRUE, 0);
								b_cancel = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
								g_signal_connect_swapped(b_cancel, "clicked", G_CALLBACK(
											_on_preferences_cancel), coder);
								gtk_box_pack_end(GTK_BOX(hbox), b_cancel, FALSE, TRUE, 0);
								gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, TRUE, 0);
								gtk_container_add(GTK_CONTAINER(coder->pr_window), vbox);
								_preferences_set(coder);
								gtk_widget_show_all(vbox);
							}
							static void _preferences_set(Coder * coder)
							{
								char const * p;
								if((p = config_get(coder->config, "editor", "command")) == NULL)
									p = "editor";
								gtk_entry_set_text(GTK_ENTRY(coder->pr_editor_command), p);
								/* FIXME implement the rest */
							}
							static void _on_preferences_apply(gpointer data)
							{
								Coder * coder = data;
								config_set(coder->config, "editor", "command", gtk_entry_get_text(
											GTK_ENTRY(coder->pr_editor_command)));
								/* FIXME implement the rest */
							}
							static void _on_preferences_cancel(gpointer data)
							{
								Coder * coder = data;
								_preferences_set(coder);
								gtk_widget_hide(coder->pr_window);
							}
							static void _on_preferences_ok(gpointer data)
							{
								Coder * coder = data;
								_on_preferences_apply(coder);
								gtk_widget_hide(coder->pr_window);
								/* FIXME actually save preferences */
							}
							/* callbacks */
							static gboolean _on_preferences_closex(gpointer data)
							{
								Coder * coder = data;
								_on_preferences_cancel(coder);
								return TRUE;
							}
							/* private */
							/* coder_get_current_project */
							static Project * _coder_get_current_project(Coder * coder)
							{
								if(coder->cur == NULL)
								{
									/* FIXME should not happen (disable callback action) */
									coder_error(coder, _("No project opened"), 1);
									return NULL;
								}
								return coder->cur;
							}
							