/* yalias: a GTK command launcher.
   Copyright 2003, 2004 Adam Sampson <azz@us-lot.org>

   yalias is free software; you can redistribute and/or modify it
   under the terms of that license as published by the Free Software
   Foundation; either version 2 of the License, or (at your option)
   any later version.

   yalias is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with yalias; see the file COPYING. If not, write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
   MA 02111-1307 USA, or see http://www.gnu.org/.
 */

#include <gtk/gtk.h>
#include <gdk/gdkx.h>
#include <gdk/gdkkeysyms.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <sys/types.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>
#include <unistd.h>

GtkWidget *window, *hbox, *entry, *clear, *clear_image, *execute,
	*execute_image;

int config_show_buttons = 1;
int config_sticky = 0;
int config_dock_hint = 0;
int config_withdrawn = 0;
int config_one_shot = 0;
int config_width = 0;
char *config_geometry = NULL;
int restart_on_quit = 0;

typedef struct {
	regex_t re;
	char *command;
} def;

#define MAXDEFS 100
def defs[MAXDEFS];
int num_defs = 0;

void die(const char *msg, const char *arg) {
	if (arg == NULL) {
		fprintf(stderr, "%s\n", msg);
	} else {
		fprintf(stderr, "%s '%s'\n", msg, arg);
	}
	exit(20);
}

char *get_match(const char *buf, regmatch_t *match) {
	int l = match->rm_eo - match->rm_so;
	char *ret = malloc(l + 1);

	strncpy(ret, buf + match->rm_so, l);
	ret[l] = '\0';

	return ret;
}

int get_bool(const char *s) {
	if (strcmp(s, "yes") == 0)
		return 1;
	else if (strcmp(s, "no") == 0)
		return 0;
	else
		die("Bad boolean value", s);
	return -1;
}

void process_config_line(char *line) {
	char *arg;

	if (line[0] == '\0' || line[0] == '#')
		return;

	arg = strchr(line, ' ');
	if (arg != NULL)
		*arg++ = '\0';

	if (strcmp(line, "match") == 0 && arg != NULL) {
		char *q = strchr(arg, '=');
		if (q == NULL)
			die("No = in config command", line);
		*q++ = '\0';
	
		if (num_defs >= MAXDEFS)
			die("Too many matches defined", line);
		if (regcomp(&defs[num_defs].re, arg, REG_EXTENDED) != 0)
			die("Bad regexp", arg);
		defs[num_defs].command = strdup(q);
		++num_defs;
	} else if (strcmp(line, "clear-matches") == 0) {
		int i;

		for (i = 0; i < num_defs; i++) {
			free(defs[i].command);
			regfree(&defs[i].re);
		}

		num_defs = 0;
	} else if (strcmp(line, "show-buttons") == 0 && arg != NULL) {
		config_show_buttons = get_bool(arg);
	} else if (strcmp(line, "sticky") == 0 && arg != NULL) {
		config_sticky = get_bool(arg);
	} else if (strcmp(line, "dock-hint") == 0 && arg != NULL) {
		config_dock_hint = get_bool(arg);
	} else if (strcmp(line, "withdrawn") == 0 && arg != NULL) {
		config_withdrawn = get_bool(arg);
	} else if (strcmp(line, "one-shot") == 0 && arg != NULL) {
		config_one_shot = get_bool(arg);
	} else if (strcmp(line, "width") == 0 && arg != NULL) {
		if (strcmp(arg, "none") == 0)
			config_width = 0;
		else
			config_width = atoi(arg);
	} else if (strcmp(line, "geometry") == 0 && arg != NULL) {
		if (config_geometry != NULL)
			free(config_geometry);
		if (strcmp(arg, "none") == 0)
			config_geometry = NULL;
		else
			config_geometry = strdup(arg);
	} else {
		die("Bad config command", line);
	}
}

int read_config(const char *name) {
	char buf[1024];
	FILE *f;

	f = fopen(name, "r");
	if (f == NULL) return 0;

	while (fgets(buf, sizeof buf, f) != NULL && num_defs < MAXDEFS) {
		buf[sizeof buf - 1] = '\0';
		buf[strlen(buf) - 1] = '\0';
		process_config_line(buf);
	}

	fclose(f);
	return 1;
}

gboolean handle_delete(GtkWidget *widget, GdkEvent *event, gpointer data) {
	return FALSE;
}

void handle_destroy(GtkWidget *widget, gpointer data) {
	gtk_main_quit();
}

void handle_clear(GtkWidget *widget, gpointer data) {
	gtk_entry_set_text(GTK_ENTRY(entry), "");
}

void reset_window() {
	gtk_entry_set_text(GTK_ENTRY(entry), "");
	gtk_widget_grab_focus(entry);
}

void run_command(char *cmd) {
	static char *last_cmd = NULL;
	GError *error = NULL;
	gchar *argv[4] = { "/bin/sh", "-c", cmd, NULL };

	if (strcmp(cmd, "!quit") == 0) {
		gtk_main_quit();
		return;
	} else if (strcmp(cmd, "!restart") == 0) {
		restart_on_quit = 1;
		gtk_main_quit();
		return;
	} else if (strcmp(cmd, "!last") == 0) {
		argv[2] = last_cmd;
	} else {
		free(last_cmd);
		last_cmd = strdup(cmd);
	}

	g_spawn_async(NULL, argv, NULL, 0, NULL, NULL, NULL, &error);

	if (config_one_shot)
		gtk_main_quit();
}

void handle_execute(GtkWidget *widget, gpointer data) {
	int i;
	const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
	char *p, *q, *end;
	char buf[1024];
#define MAXMATCHES 10
	regmatch_t match[MAXMATCHES];

	for (i = 0; i < num_defs; i++) {
		if (regexec(&defs[i].re, text, MAXMATCHES, match, 0)
			  != REG_NOMATCH)
			goto found;
	}
	return;

found:
	p = defs[i].command;
	q = buf;

	/* Eurgh. TODO: port this to use iolib. */

	end = buf + sizeof buf - 1;
	while (q < end) {
		char c = *p++;
		if (c == '\0') {
			break;
		} else if (c == '$') {
			int j, n;
			char *sub;

			c = *p++;
			if (c == '\0') {
				break;
			} else if (c == '$') {
				*q++ = c;
			} else if (c >= '0' && c <= '9') {
				j = c - '0';
				if (match[j].rm_so >= 0)
					sub = get_match(text, &match[j]);
				else
					sub = strdup("");
				n = strlen(sub);
				if (q + n > end) break;
				strcpy(q, sub);
				free(sub);
				q += n;
			} else {
				if (q + 2 > end) break;
				*q++ = '$';
				*q++ = c;
			}
		} else {
			*q++ = c;
		}
	}
	*q = '\0';

	run_command(buf);
	reset_window();
}

void handle_activate(GtkEntry *entry, gpointer user_data) {
	handle_execute(GTK_WIDGET(entry), user_data);
}

void version() {
	printf("yalias " VERSION " by Adam Sampson\n");
}

void usage() {
	version();
	printf("Usage: yalias [OPTION]...\n\n"
	       "-c FILE                          Read config file FILE\n"
	       "-o LINE                          Process config line LINE\n"
	       "-V                               Display version and exit\n"
	       "-h                               Display this help and exit\n\n"
	       "Report bugs to <azz@us-lot.org>.\n");
}

int main(int argc, char **argv) {
	GtkAccelGroup *accel_group;
	const char *home = getenv("HOME");
	int got_config = 0;

	gtk_init(&argc, &argv);

	got_config |= read_config(SYSTEM_CONFIG);
	if (home != NULL) {
		char s[1024];
		snprintf(s, sizeof s, "%s/.yaliasrc", home);
		got_config |= read_config(s);
	}

	opterr = 0;
	while (1) {
		int c = getopt(argc, argv, "o:c:Vh");
		if (c == -1) break;

		switch (c) {
		case 'o':
			process_config_line(optarg);
			break;
		case 'c':
			if (!read_config(optarg))
				die("Cannot read config", optarg);
			got_config |= 1;
			break;
		case 'V':
			version();
			return 0;
		default:
			usage();
			return 20;
		}
	}

	if (!got_config)
		die("Unable to find a config file.", NULL);

	entry = gtk_entry_new();
	gtk_entry_set_activates_default(GTK_ENTRY(entry), FALSE);
	g_signal_connect(G_OBJECT(entry), "activate",
	                 G_CALLBACK(handle_activate), NULL);

	accel_group = gtk_accel_group_new();

	clear_image = gtk_image_new_from_stock(GTK_STOCK_CLEAR,
	                                       GTK_ICON_SIZE_BUTTON);

	clear = gtk_button_new();
	g_signal_connect(G_OBJECT(clear), "clicked",
	                 G_CALLBACK(handle_clear), NULL);
	gtk_widget_add_accelerator(clear, "clicked", accel_group,
	                           GDK_Escape, 0, 0);
	gtk_container_add(GTK_CONTAINER(clear), clear_image);

	execute_image = gtk_image_new_from_stock(GTK_STOCK_EXECUTE,
	                                         GTK_ICON_SIZE_BUTTON);

	execute = gtk_button_new();
	g_signal_connect(G_OBJECT(execute), "clicked",
	                 G_CALLBACK(handle_execute), NULL);
	gtk_container_add(GTK_CONTAINER(execute), execute_image);

	hbox = gtk_hbox_new(FALSE, 0);
	if (config_show_buttons) {
		gtk_box_pack_start(GTK_BOX(hbox), clear, FALSE, TRUE, 0);
		gtk_box_pack_end(GTK_BOX(hbox), execute, FALSE, TRUE, 0);
	}
	gtk_box_pack_start(GTK_BOX(hbox), entry, TRUE, TRUE, 0);
	gtk_widget_show_all(hbox);

	window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_window_set_title(GTK_WINDOW(window), "yalias");
	g_signal_connect(G_OBJECT(window), "delete_event",
	                 G_CALLBACK(handle_delete), NULL);
	g_signal_connect(G_OBJECT(window), "destroy",
	                 G_CALLBACK(handle_destroy), NULL);
	gtk_window_add_accel_group(GTK_WINDOW(window), accel_group);
	gtk_container_add(GTK_CONTAINER(window), hbox);

	if (config_width != 0)
		gtk_widget_set_size_request(window, config_width, -1);
	if (config_geometry != NULL)
		gtk_window_parse_geometry(GTK_WINDOW(window), config_geometry);
	gtk_window_set_resizable(GTK_WINDOW(window), FALSE);
	if (config_sticky)
		gtk_window_stick(GTK_WINDOW(window));
	if (config_dock_hint)
		gtk_window_set_type_hint(GTK_WINDOW(window),
		                         GDK_WINDOW_TYPE_HINT_DOCK);
	gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
	gtk_widget_realize(window);

	if (config_withdrawn) {
		Display *xdisp = GDK_WINDOW_XDISPLAY(window->window);
		Window xwin = GDK_WINDOW_XWINDOW(window->window);
		XWMHints hints;

		hints.initial_state = WithdrawnState;
		hints.flags = StateHint;

		XSetWMHints(xdisp, xwin, &hints);
	}

	gtk_widget_show_all(window);

	reset_window();

	gtk_main();

	if (restart_on_quit)
		execvp(argv[0], argv);

	return 0;
}

