1#define _GNU_SOURCE
  2#define _POSIX_C_SOURCE 200809L
  3#include <stdio.h>
  4#include <stdlib.h>
  5#include <string.h>
  6#include <dirent.h>
  7#include <ctype.h>
  8
  9#include <X11/Xlib.h>
 10#include <X11/Xutil.h>
 11#include <X11/keysym.h>
 12
 13#include <sys/stat.h>
 14#include <gio/gio.h>
 15
 16#include "glitch.h"
 17#include "config.h"
 18
 19extern WindowManager wm;
 20
 21static void launcher_filter(void);
 22
 23static int compare_launcher_items(const void *a, const void *b) {
 24	const LauncherItem *ia = (const LauncherItem *)a;
 25	const LauncherItem *ib = (const LauncherItem *)b;
 26	if (ib->usage != ia->usage)
 27		return ib->usage - ia->usage;
 28	return strcasecmp(ia->name, ib->name);
 29}
 30
 31static void load_usage(void) {
 32	char path[1024];
 33	char *home = getenv("HOME");
 34	if (!home) return;
 35	snprintf(path, sizeof(path), "%s/.cache/glitch/usage.db", home);
 36
 37	GKeyFile *kf = g_key_file_new();
 38	if (g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, NULL)) {
 39		for (int i = 0; i < wm.launcher_items_count; i++) {
 40			wm.launcher_items[i].usage = g_key_file_get_integer(kf, "Usage", wm.launcher_items[i].exec, NULL);
 41		}
 42	}
 43	g_key_file_free(kf);
 44}
 45
 46static void record_usage(const char *exec) {
 47	char path[1024];
 48	char *home = getenv("HOME");
 49	if (!home) return;
 50	snprintf(path, sizeof(path), "%s/.cache/glitch", home);
 51	mkdir(path, 0755);
 52	snprintf(path, sizeof(path), "%s/.cache/glitch/usage.db", home);
 53
 54	GKeyFile *kf = g_key_file_new();
 55	g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, NULL);
 56
 57	int count = g_key_file_get_integer(kf, "Usage", exec, NULL);
 58	g_key_file_set_integer(kf, "Usage", exec, count + 1);
 59
 60	g_key_file_save_to_file(kf, path, NULL);
 61	g_key_file_free(kf);
 62}
 63
 64static void load_applications(void) {
 65	if (wm.launcher_items) {
 66		for (int i = 0; i < wm.launcher_items_count; i++) {
 67			free(wm.launcher_items[i].name);
 68			free(wm.launcher_items[i].exec);
 69		}
 70		free(wm.launcher_items);
 71		wm.launcher_items = NULL;
 72		wm.launcher_items_count = 0;
 73	}
 74
 75	GList *apps = g_app_info_get_all();
 76	int total_apps = g_list_length(apps);
 77	wm.launcher_items = malloc(sizeof(LauncherItem) * total_apps);
 78
 79	for (GList *l = apps; l != NULL; l = l->next) {
 80		GAppInfo *app = (GAppInfo *)l->data;
 81
 82		if (!g_app_info_should_show(app)) {
 83			g_object_unref(app);
 84			continue;
 85		}
 86
 87		const char *name = g_app_info_get_name(app);
 88		const char *exec = g_app_info_get_commandline(app);
 89
 90		if (name && exec) {
 91			wm.launcher_items[wm.launcher_items_count].name = strdup(name);
 92			wm.launcher_items[wm.launcher_items_count].usage = 0;
 93
 94			char *e = strdup(exec);
 95
 96			char *percent = strchr(e, '%');
 97			if (percent) *percent = '\0';
 98
 99			// Trim potential trailing space after removing %
100			int len = strlen(e);
101			while (len > 0 && isspace((unsigned char)e[len-1])) {
102				e[--len] = '\0';
103			}
104
105			wm.launcher_items[wm.launcher_items_count].exec = strdup(e);
106			free(e);
107
108			wm.launcher_items_count++;
109		}
110		g_object_unref(app);
111	}
112	g_list_free(apps);
113
114	load_usage();
115	qsort(wm.launcher_items, wm.launcher_items_count, sizeof(LauncherItem), compare_launcher_items);
116}
117
118void toggle_launcher(const Arg *arg) {
119	(void)arg;
120	if (wm.launcher_active) {
121		wm.launcher_active = 0;
122		XUnmapWindow(wm.dpy, wm.launcher_win);
123		XUngrabKeyboard(wm.dpy, CurrentTime);
124		return;
125	}
126
127	load_applications();
128
129	wm.launcher_active = 1;
130	wm.launcher_search[0] = '\0';
131	wm.launcher_selected = 0;
132	launcher_filter();
133
134	int screen_width = DisplayWidth(wm.dpy, wm.screen);
135	int screen_height = DisplayHeight(wm.dpy, wm.screen);
136	int win_width = launcher_width;
137	int win_height = launcher_height;
138	int x = (screen_width - win_width) / 2;
139	int y = (screen_height - win_height) / 2;
140
141	if (!wm.launcher_win) {
142		XSetWindowAttributes wa;
143		wa.override_redirect = True;
144		wa.background_pixel = wm.xft_launcher_bg.pixel;
145		wa.border_pixel = wm.xft_launcher_border.pixel;
146		wm.launcher_win = XCreateWindow(wm.dpy, wm.root, x, y, win_width, win_height, 2,
147				DefaultDepth(wm.dpy, wm.screen), CopyFromParent,
148				DefaultVisual(wm.dpy, wm.screen),
149				CWOverrideRedirect | CWBackPixel | CWBorderPixel, &wa);
150	} else {
151		XMoveWindow(wm.dpy, wm.launcher_win, x, y);
152	}
153
154	XMapRaised(wm.dpy, wm.launcher_win);
155	XGrabKeyboard(wm.dpy, wm.launcher_win, True, GrabModeAsync, GrabModeAsync, CurrentTime);
156	launcher_draw();
157}
158
159static void launcher_filter(void) {
160	if (wm.launcher_filtered) free(wm.launcher_filtered);
161	wm.launcher_filtered = malloc(sizeof(LauncherItem *) * wm.launcher_items_count);
162	wm.launcher_filtered_count = 0;
163
164	for (int i = 0; i < wm.launcher_items_count; i++) {
165		if (wm.launcher_search[0] == '\0' || 
166				strcasestr(wm.launcher_items[i].name, wm.launcher_search)) {
167			wm.launcher_filtered[wm.launcher_filtered_count++] = &wm.launcher_items[i];
168		}
169	}
170
171	if (wm.launcher_selected >= wm.launcher_filtered_count) {
172		wm.launcher_selected = wm.launcher_filtered_count > 0 ? wm.launcher_filtered_count - 1 : 0;
173	}
174}
175
176void launcher_handle_key(void) {
177	KeySym keysym = XLookupKeysym(&wm.ev.xkey, 0);
178	int len = strlen(wm.launcher_search);
179
180	if (keysym == XK_Escape) {
181		toggle_launcher(NULL);
182		return;
183	} else if (keysym == XK_BackSpace) {
184		if (len > 0) {
185			wm.launcher_search[len - 1] = '\0';
186			wm.launcher_selected = 0;
187			launcher_filter();
188		}
189	} else if (keysym == XK_Return) {
190		if (wm.launcher_filtered_count > 0 && wm.launcher_selected < wm.launcher_filtered_count) {
191			record_usage(wm.launcher_filtered[wm.launcher_selected]->exec);
192			execute_shortcut(wm.launcher_filtered[wm.launcher_selected]->exec);
193			toggle_launcher(NULL);
194			return;
195		}
196	} else if (keysym == XK_Up) {
197		if (wm.launcher_selected > 0) wm.launcher_selected--;
198	} else if (keysym == XK_Down) {
199		if (wm.launcher_selected < wm.launcher_filtered_count - 1) wm.launcher_selected++;
200	} else {
201		char buf[32];
202		int n = XLookupString(&wm.ev.xkey, buf, sizeof(buf), NULL, NULL);
203		if (n > 0 && len + n < (int)sizeof(wm.launcher_search)) {
204			memcpy(wm.launcher_search + len, buf, n);
205			wm.launcher_search[len + n] = '\0';
206			wm.launcher_selected = 0;
207			launcher_filter();
208		}
209	}
210
211	launcher_draw();
212}
213
214void launcher_draw(void) {
215	if (!wm.launcher_win) return;
216
217	XftDraw *draw = XftDrawCreate(wm.dpy, wm.launcher_win, DefaultVisual(wm.dpy, wm.screen), wm.cmap);
218	XWindowAttributes wa;
219	XGetWindowAttributes(wm.dpy, wm.launcher_win, &wa);
220
221	// Clear background
222	XftDrawRect(draw, &wm.xft_launcher_bg, 0, 0, wa.width, wa.height);
223
224	int x = 20;
225	int y = 30;
226	int row_height = wm.launcher_font->height + 10;
227
228	// Draw search bar
229	char search_display[300];
230	snprintf(search_display, sizeof(search_display), "> %s", wm.launcher_search);
231	XftDrawStringUtf8(draw, &wm.xft_launcher_fg, wm.launcher_font, x, y, (FcChar8 *)search_display, strlen(search_display));
232
233	y += row_height + 10; // Extra padding below input
234
235	// Draw items
236	int start_idx = 0;
237	if (wm.launcher_selected >= 10) {
238		start_idx = wm.launcher_selected - 9;
239	}
240
241	for (int i = start_idx; i < wm.launcher_filtered_count && i < start_idx + 15; i++) {
242		if (i == wm.launcher_selected) {
243			XftDrawRect(draw, &wm.xft_launcher_hl_bg, 0, y - wm.launcher_font->ascent - 5, wa.width, row_height);
244			XftDrawStringUtf8(draw, &wm.xft_launcher_hl_fg, wm.launcher_font, x, y, (FcChar8 *)wm.launcher_filtered[i]->name, strlen(wm.launcher_filtered[i]->name));
245		} else {
246			XftDrawStringUtf8(draw, &wm.xft_launcher_fg, wm.launcher_font, x, y, (FcChar8 *)wm.launcher_filtered[i]->name, strlen(wm.launcher_filtered[i]->name));
247		}
248		y += row_height;
249	}
250
251	XftDrawDestroy(draw);
252	XFlush(wm.dpy);
253}