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}