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}