1#define TB_IMPL
2#include "termbox2.h"
3
4#include <gio/gdesktopappinfo.h>
5#include <gio/gio.h>
6
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
10
11#include "mimetypes.h"
12
13// Configuration Constants
14#define COL_WIDTH_CATEGORIES 22
15#define COL_WIDTH_APPS 40
16#define X_OFF_CATEGORIES 2
17#define X_OFF_APPS (X_OFF_CATEGORIES + COL_WIDTH_CATEGORIES)
18#define X_OFF_FILE (X_OFF_APPS + COL_WIDTH_APPS + 2)
19#define Y_OFF_START 3
20#define Y_OFF_TITLES 1
21#define Y_OFF_FOOTER 2
22
23// Color Scheme
24#define COLOR_TITLE (TB_YELLOW | TB_BOLD)
25#define COLOR_SELECTED TB_BLUE
26#define COLOR_DEFAULT TB_WHITE
27#define COLOR_BG TB_BLACK
28#define COLOR_DIM TB_WHITE
29#define COLOR_SUCCESS TB_GREEN
30#define COLOR_ERROR TB_RED
31
32typedef struct {
33 int category_idx;
34 int app_idx;
35 int category_offset;
36 int app_offset;
37 int col; // 0 for category, 1 for app
38 char message[512];
39 GList *cached_apps;
40 int is_dev_mode;
41 int dev_count;
42} State;
43
44int get_category_count(State *state) {
45 if (state->is_dev_mode) {
46 return state->dev_count;
47 }
48 int count = 0;
49 while (categories[count].name)
50 count++;
51 return count;
52}
53
54const char *get_category_name(State *state, int idx) {
55 if (state->is_dev_mode) {
56 static char mock_name[64];
57 snprintf(mock_name, sizeof(mock_name), "Dev Category %d", idx + 1);
58 return mock_name;
59 }
60 return categories[idx].name;
61}
62
63GList *get_apps_for_category(State *state, int category_idx) {
64 GList *apps = NULL;
65 for (int m = 0; categories[category_idx].mimetypes[m] != NULL; ++m) {
66 GList *type_apps = g_app_info_get_all_for_type(categories[category_idx].mimetypes[m]);
67 for (GList *l = type_apps; l != NULL; l = l->next) {
68 GAppInfo *app = (GAppInfo *)l->data;
69 int found = 0;
70 for (GList *a = apps; a != NULL; a = a->next) {
71 if (g_app_info_equal(app, (GAppInfo *)a->data)) {
72 found = 1;
73 break;
74 }
75 }
76 if (!found) {
77 apps = g_list_append(apps, g_object_ref(app));
78 }
79 }
80 g_list_free_full(type_apps, g_object_unref);
81 }
82 return apps;
83}
84
85void update_cached_apps(State *state) {
86 if (state->cached_apps && !state->is_dev_mode) {
87 g_list_free_full(state->cached_apps, g_object_unref);
88 } else if (state->cached_apps && state->is_dev_mode) {
89 g_list_free_full(state->cached_apps, g_free);
90 }
91 state->cached_apps = NULL;
92
93 if (state->is_dev_mode) {
94 for (int i = 0; i < state->dev_count; ++i) {
95 char *mock_app = g_strdup_printf("Dev Application %d.%d", state->category_idx + 1, i + 1);
96 state->cached_apps = g_list_append(state->cached_apps, mock_app);
97 }
98 } else {
99 state->cached_apps = get_apps_for_category(state, state->category_idx);
100 }
101 state->app_idx = 0;
102}
103
104void draw_titles() {
105 tb_print(X_OFF_CATEGORIES, Y_OFF_TITLES, COLOR_TITLE, COLOR_BG, "CATEGORIES");
106 tb_print(X_OFF_APPS, Y_OFF_TITLES, COLOR_TITLE, COLOR_BG, "APPLICATIONS");
107 tb_print(X_OFF_FILE, Y_OFF_TITLES, COLOR_TITLE, COLOR_BG, "FILE");
108}
109
110void draw_categories(State *state) {
111 int count = get_category_count(state);
112 int height = tb_height() - Y_OFF_START - Y_OFF_FOOTER;
113 for (int i = 0; i < height && (i + state->category_offset) < count; ++i) {
114 int idx = i + state->category_offset;
115 uint16_t fg = COLOR_DEFAULT;
116 uint16_t bg = COLOR_BG;
117 if (state->col == 0 && state->category_idx == idx) {
118 bg = COLOR_SELECTED;
119 } else if (state->category_idx == idx) {
120 fg = COLOR_SELECTED;
121 bg = COLOR_DEFAULT;
122 }
123 tb_print(X_OFF_CATEGORIES, Y_OFF_START + i, fg, bg, get_category_name(state, idx));
124 }
125}
126
127void draw_apps_list(State *state) {
128 int height = tb_height() - Y_OFF_START - Y_OFF_FOOTER;
129 if (state->is_dev_mode) {
130 int i = 0;
131 GList *l = g_list_nth(state->cached_apps, state->app_offset);
132 for (; l != NULL && i < height; l = l->next, ++i) {
133 int idx = i + state->app_offset;
134 char *app_name = (char *)l->data;
135 uint16_t fg = COLOR_DEFAULT;
136 uint16_t bg = COLOR_BG;
137 if (state->col == 1 && state->app_idx == idx) {
138 bg = COLOR_SELECTED;
139 }
140 char name[256];
141 snprintf(name, sizeof(name), " %s", app_name);
142 tb_print(X_OFF_APPS, Y_OFF_START + i, fg, bg, name);
143 }
144 return;
145 }
146 GList *defaults = NULL;
147 for (int m = 0; categories[state->category_idx].mimetypes[m] != NULL; ++m) {
148 GAppInfo *d = g_app_info_get_default_for_type(categories[state->category_idx].mimetypes[m], FALSE);
149 if (d) {
150 int found = 0;
151 for (GList *l = defaults; l != NULL; l = l->next) {
152 if (g_app_info_equal(d, (GAppInfo *)l->data)) {
153 found = 1;
154 break;
155 }
156 }
157 if (!found) {
158 defaults = g_list_append(defaults, d);
159 } else {
160 g_object_unref(d);
161 }
162 }
163 }
164
165 int i = 0;
166 GList *l = g_list_nth(state->cached_apps, state->app_offset);
167 for (; l != NULL && i < height; l = l->next, ++i) {
168 int idx = i + state->app_offset;
169 GAppInfo *app = (GAppInfo *)l->data;
170 uint16_t fg = COLOR_DEFAULT;
171 uint16_t bg = COLOR_BG;
172 if (state->col == 1 && state->app_idx == idx) {
173 bg = COLOR_SELECTED;
174 }
175
176 int is_default = 0;
177 for (GList *d = defaults; d != NULL; d = d->next) {
178 if (g_app_info_equal(app, (GAppInfo *)d->data)) {
179 is_default = 1;
180 break;
181 }
182 }
183
184 char name[256];
185 snprintf(name, sizeof(name), "%s %s", is_default ? "*" : " ", g_app_info_get_name(app));
186 tb_print(X_OFF_APPS, Y_OFF_START + i, fg, bg, name);
187
188 if (G_IS_DESKTOP_APP_INFO(app)) {
189 const char *filename = g_desktop_app_info_get_filename(G_DESKTOP_APP_INFO(app));
190 if (filename) {
191 tb_print(X_OFF_FILE, Y_OFF_START + i, COLOR_DIM, bg, filename);
192 }
193 }
194 }
195 g_list_free_full(defaults, g_object_unref);
196}
197
198void draw(State *state) {
199 tb_clear();
200 draw_titles();
201 draw_categories(state);
202 draw_apps_list(state);
203
204 if (state->message[0] != '\0' || state->is_dev_mode) {
205 uint16_t msg_col = COLOR_SUCCESS;
206 const char *msg = state->message;
207 if (state->message[0] == '\0' && state->is_dev_mode) {
208 msg = "Developer Mode";
209 }
210 if (strncmp(msg, "Failed", 6) == 0) {
211 msg_col = COLOR_ERROR;
212 }
213 tb_print(X_OFF_CATEGORIES, tb_height() - 1, msg_col, COLOR_BG, msg);
214 }
215
216 tb_present();
217}
218
219int main() {
220 if (tb_init() != 0) {
221 return 1;
222 }
223 tb_set_clear_attrs(COLOR_DEFAULT, COLOR_BG);
224
225 State state = {0, 0, 0, 0, 0, {0}, NULL, 0, 0};
226 char *dev_env = getenv("XDGCTL_DEV");
227 if (dev_env) {
228 state.is_dev_mode = 1;
229 state.dev_count = atoi(dev_env);
230 if (state.dev_count <= 0)
231 state.dev_count = 10;
232 }
233
234 update_cached_apps(&state);
235
236 struct tb_event ev;
237 while (1) {
238 int visible_height = tb_height() - Y_OFF_START - Y_OFF_FOOTER;
239 if (state.category_idx < state.category_offset) {
240 state.category_offset = state.category_idx;
241 } else if (state.category_idx >= state.category_offset + visible_height) {
242 state.category_offset = state.category_idx - visible_height + 1;
243 }
244 if (state.app_idx < state.app_offset) {
245 state.app_offset = state.app_idx;
246 } else if (state.app_idx >= state.app_offset + visible_height) {
247 state.app_offset = state.app_idx - visible_height + 1;
248 }
249
250 draw(&state);
251 tb_poll_event(&ev);
252
253 if (ev.type == TB_EVENT_KEY) {
254 if (ev.key == TB_KEY_ESC || ev.ch == 'q') {
255 break;
256 }
257
258 if (ev.key == TB_KEY_ARROW_UP) {
259 if (state.col == 0) {
260 if (state.category_idx > 0) {
261 state.category_idx--;
262 update_cached_apps(&state);
263 state.app_offset = 0;
264 state.message[0] = '\0';
265 }
266 } else {
267 if (state.app_idx > 0) {
268 state.app_idx--;
269 state.message[0] = '\0';
270 }
271 }
272 } else if (ev.key == TB_KEY_ARROW_DOWN) {
273 if (state.col == 0) {
274 int count = get_category_count(&state);
275 if (state.category_idx < count - 1) {
276 state.category_idx++;
277 update_cached_apps(&state);
278 state.app_offset = 0;
279 state.message[0] = '\0';
280 }
281 } else {
282 int count = g_list_length(state.cached_apps);
283 if (state.app_idx < count - 1) {
284 state.app_idx++;
285 state.message[0] = '\0';
286 }
287 }
288 } else if (ev.key == TB_KEY_ARROW_RIGHT || ev.key == TB_KEY_TAB) {
289 if (state.col == 0) {
290 state.col = 1;
291 state.app_idx = 0;
292 state.app_offset = 0;
293 }
294 } else if (ev.key == TB_KEY_ARROW_LEFT) {
295 if (state.col == 1) {
296 state.col = 0;
297 }
298 } else if (ev.key == TB_KEY_ENTER) {
299 if (state.col == 1) {
300 if (state.is_dev_mode) {
301 char *selected_app = (char *)g_list_nth_data(state.cached_apps, state.app_idx);
302 if (selected_app) {
303 snprintf(state.message, sizeof(state.message),
304 "Mock: %s is now default for %s",
305 selected_app, get_category_name(&state, state.category_idx));
306 }
307 } else {
308 GAppInfo *selected_app = (GAppInfo *)g_list_nth_data(state.cached_apps, state.app_idx);
309 if (selected_app) {
310 snprintf(state.message, sizeof(state.message),
311 "%s is now default %s",
312 g_app_info_get_name(selected_app), categories[state.category_idx].name);
313 for (int m = 0; categories[state.category_idx].mimetypes[m] != NULL; ++m) {
314 GError *error = NULL;
315 g_app_info_set_as_default_for_type(selected_app, categories[state.category_idx].mimetypes[m], &error);
316 if (error) {
317 snprintf(state.message, sizeof(state.message), "Failed to set default application");
318 g_error_free(error);
319 break;
320 }
321 }
322 }
323 }
324 }
325 }
326 }
327 }
328
329 if (state.cached_apps && !state.is_dev_mode) {
330 g_list_free_full(state.cached_apps, g_object_unref);
331 } else if (state.cached_apps && state.is_dev_mode) {
332 g_list_free_full(state.cached_apps, g_free);
333 }
334 tb_shutdown();
335 return 0;
336}