#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include typedef struct { char *desktop_path; char *name; GDesktopAppInfo *app; } AppEntry; typedef struct { GPtrArray *entries; GHashTable *seen_paths; GdkCursor *cursor_default; GdkCursor *cursor_hand; GtkWidget *window; GtkWidget *list; GtkWidget *status_label; } LauncherState; // nav: limits enum { MAX_INPUT_LINE_BYTES = 4096, MAX_DESKTOP_ENTRIES = 4096, MAX_APP_NAME_CHARS = 160 }; // nav: state cleanup static void launcher_state_cleanup(LauncherState *state) { if (state == NULL) { return; } if (state->entries != NULL) { g_ptr_array_free(state->entries, TRUE); state->entries = NULL; } if (state->seen_paths != NULL) { g_hash_table_destroy(state->seen_paths); state->seen_paths = NULL; } g_clear_object(&state->cursor_default); g_clear_object(&state->cursor_hand); } static void app_entry_free(gpointer data) { AppEntry *entry = (AppEntry *)data; if (entry == NULL) { return; } g_clear_object(&entry->app); g_free(entry->desktop_path); g_free(entry->name); g_free(entry); } static char *trim_in_place(char *text) { char *start = text; char *end = NULL; while (*start != '\0' && g_ascii_isspace((guchar)*start)) { start++; } if (*start == '\0') { return start; } end = start + strlen(start) - 1; while (end > start && g_ascii_isspace((guchar)*end)) { end--; } end[1] = '\0'; return start; } // nav: path helpers static char *expand_path(const char *path) { if (path == NULL || *path == '\0') { return NULL; } if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) { const char *home = g_get_home_dir(); if (home == NULL || *home == '\0') { return NULL; } if (path[1] == '\0') { return g_strdup(home); } return g_build_filename(home, path + 2, NULL); } return g_strdup(path); } static char *normalize_desktop_path(const char *raw_path) { char *expanded = NULL; char *canonical = NULL; expanded = expand_path(raw_path); if (expanded == NULL) { return NULL; } canonical = g_canonicalize_filename(expanded, NULL); g_free(expanded); if (canonical == NULL || !g_path_is_absolute(canonical)) { g_free(canonical); return NULL; } return canonical; } static char *build_safe_display_name(GDesktopAppInfo *app, const char *fallback_path) { const char *name = g_app_info_get_display_name(G_APP_INFO(app)); char *safe_name = NULL; if (name == NULL || *name == '\0') { name = g_app_info_get_name(G_APP_INFO(app)); } if (name == NULL || *name == '\0') { name = fallback_path; } safe_name = g_utf8_make_valid(name != NULL ? name : "app", -1); if (safe_name == NULL || *safe_name == '\0') { g_free(safe_name); safe_name = g_strdup("app"); } if (g_utf8_strlen(safe_name, -1) > MAX_APP_NAME_CHARS) { char *truncated = g_utf8_substring(safe_name, 0, MAX_APP_NAME_CHARS); g_free(safe_name); safe_name = truncated; } return safe_name; } // nav: desktop entry loading static gboolean add_desktop_path(LauncherState *state, const char *raw_path) { char *path = NULL; GDesktopAppInfo *app = NULL; char *safe_name = NULL; AppEntry *entry = NULL; if (state->entries->len >= MAX_DESKTOP_ENTRIES) { return FALSE; } path = normalize_desktop_path(raw_path); if (path == NULL) { return FALSE; } if (!g_str_has_suffix(path, ".desktop")) { g_free(path); return FALSE; } if (g_hash_table_contains(state->seen_paths, path)) { g_free(path); return FALSE; } if (!g_file_test(path, G_FILE_TEST_IS_REGULAR) || access(path, R_OK) != 0) { g_free(path); return FALSE; } app = g_desktop_app_info_new_from_filename(path); if (app == NULL) { g_free(path); return FALSE; } safe_name = build_safe_display_name(app, path); if (safe_name == NULL) { g_clear_object(&app); g_free(path); return FALSE; } entry = g_new0(AppEntry, 1); entry->desktop_path = path; entry->name = safe_name; entry->app = app; g_ptr_array_add(state->entries, entry); g_hash_table_add(state->seen_paths, g_strdup(path)); return TRUE; } // nav: input loading static void load_paths_from_stream(LauncherState *state, FILE *stream, const char *source_name) { char line[MAX_INPUT_LINE_BYTES + 2]; guint line_no = 0; while (fgets(line, sizeof(line), stream) != NULL) { char *trimmed = NULL; size_t line_len = strlen(line); gboolean long_line = FALSE; line_no++; if (line_len > 0 && line[line_len - 1] == '\n') { line[line_len - 1] = '\0'; } else if (!feof(stream)) { int ch = 0; long_line = TRUE; while ((ch = fgetc(stream)) != '\n' && ch != EOF) { } } if (long_line) { g_printerr("skipping long line at %s:%u\n", source_name, line_no); continue; } trimmed = trim_in_place(line); if (*trimmed == '\0' || *trimmed == '#') { continue; } if (!add_desktop_path(state, trimmed)) { g_printerr("skipping invalid desktop path at %s:%u -> %s\n", source_name, line_no, trimmed); } if (state->entries->len >= MAX_DESKTOP_ENTRIES) { g_printerr("entry limit reached (%u)\n", MAX_DESKTOP_ENTRIES); break; } } if (ferror(stream)) { g_printerr("read error from %s: %s\n", source_name, g_strerror(errno)); } } static gboolean load_paths_from_stdin(LauncherState *state) { guint old_len = 0; if (isatty(STDIN_FILENO)) { return FALSE; } old_len = state->entries->len; load_paths_from_stream(state, stdin, "stdin"); return state->entries->len > old_len; } static char *find_config_path(void) { const char *xdg_config_home = g_getenv("XDG_CONFIG_HOME"); const char *home = g_get_home_dir(); char *path = NULL; if (xdg_config_home != NULL && *xdg_config_home != '\0') { path = g_build_filename(xdg_config_home, "dapp.conf", NULL); if (g_file_test(path, G_FILE_TEST_IS_REGULAR)) { return path; } g_free(path); } if (home != NULL && *home != '\0') { path = g_build_filename(home, ".config", "dapp.conf", NULL); if (g_file_test(path, G_FILE_TEST_IS_REGULAR)) { return path; } g_free(path); } return NULL; } static gboolean load_paths_from_config(LauncherState *state) { char *path = NULL; FILE *config = NULL; guint old_len = state->entries->len; path = find_config_path(); if (path == NULL) { return FALSE; } config = fopen(path, "r"); if (config == NULL) { g_printerr("could not read config file: %s\n", path); g_free(path); return FALSE; } load_paths_from_stream(state, config, path); if (fclose(config) != 0) { g_printerr("warning: failed to close config file: %s\n", path); } g_free(path); return state->entries->len > old_len; } // nav: ui events static gboolean on_delete_event(GtkWidget *widget, GdkEvent *event, gpointer user_data) { (void)widget; (void)event; (void)user_data; return TRUE; } static void set_status(LauncherState *state, const char *message) { gtk_label_set_text(GTK_LABEL(state->status_label), message != NULL ? message : ""); } // nav: cursor helpers static void initialize_cursors(LauncherState *state) { GdkDisplay *display = NULL; if (state == NULL) { return; } display = gdk_display_get_default(); if (display == NULL) { return; } state->cursor_default = gdk_cursor_new_from_name(display, "default"); if (state->cursor_default == NULL) { state->cursor_default = gdk_cursor_new_from_name(display, "left_ptr"); } state->cursor_hand = gdk_cursor_new_from_name(display, "pointer"); if (state->cursor_hand == NULL) { state->cursor_hand = gdk_cursor_new_from_name(display, "hand2"); } } static void set_list_cursor(LauncherState *state, gboolean use_hand) { GdkWindow *window = NULL; GdkCursor *cursor = NULL; if (state == NULL || state->list == NULL) { return; } if (!gtk_widget_get_realized(state->list)) { return; } window = gtk_widget_get_window(state->list); if (window == NULL) { return; } cursor = use_hand ? state->cursor_hand : state->cursor_default; gdk_window_set_cursor(window, cursor); } static void launch_entry(LauncherState *state, AppEntry *entry) { GError *error = NULL; if (entry == NULL) { return; } if (!g_app_info_launch(G_APP_INFO(entry->app), NULL, NULL, &error)) { if (error != NULL) { char *full_message = g_strdup_printf("launch failed: %s", error->message); set_status(state, full_message); g_free(full_message); g_clear_error(&error); } else { set_status(state, "launch failed"); } return; } set_status(state, ""); } static void activate_selected(LauncherState *state) { GtkListBoxRow *row = gtk_list_box_get_selected_row(GTK_LIST_BOX(state->list)); AppEntry *entry = NULL; if (row == NULL) { return; } entry = (AppEntry *)g_object_get_data(G_OBJECT(row), "entry"); launch_entry(state, entry); } static void move_selection(LauncherState *state, gint delta) { gint count = (gint)state->entries->len; GtkListBox *list = GTK_LIST_BOX(state->list); GtkListBoxRow *current = gtk_list_box_get_selected_row(list); gint next_index = 0; GtkListBoxRow *next_row = NULL; if (count == 0) { return; } if (current == NULL) { next_index = 0; } else { gint current_index = gtk_list_box_row_get_index(current); next_index = CLAMP(current_index + delta, 0, count - 1); } next_row = gtk_list_box_get_row_at_index(list, next_index); if (next_row != NULL) { gtk_list_box_select_row(list, next_row); gtk_widget_grab_focus(GTK_WIDGET(list)); set_list_cursor(state, TRUE); } } static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { LauncherState *state = (LauncherState *)user_data; guint key = gdk_keyval_to_lower(event->keyval); (void)widget; if (key == GDK_KEY_Escape) { return TRUE; } if (key == GDK_KEY_F4 && (event->state & GDK_MOD1_MASK)) { return TRUE; } if (key == GDK_KEY_Up || key == GDK_KEY_k || key == GDK_KEY_w) { move_selection(state, -1); return TRUE; } if (key == GDK_KEY_Down || key == GDK_KEY_j || key == GDK_KEY_s) { move_selection(state, 1); return TRUE; } if (key == GDK_KEY_Return || key == GDK_KEY_KP_Enter || key == GDK_KEY_space) { activate_selected(state); return TRUE; } return FALSE; } static void on_row_activated(GtkListBox *box, GtkListBoxRow *row, gpointer user_data) { LauncherState *state = (LauncherState *)user_data; AppEntry *entry = NULL; (void)box; if (row == NULL) { return; } entry = (AppEntry *)g_object_get_data(G_OBJECT(row), "entry"); launch_entry(state, entry); } static gboolean on_list_motion(GtkWidget *widget, GdkEventMotion *event, gpointer user_data) { GtkListBox *list = GTK_LIST_BOX(widget); LauncherState *state = (LauncherState *)user_data; GtkListBoxRow *row = NULL; GtkListBoxRow *current = NULL; row = gtk_list_box_get_row_at_y(list, (gint)event->y); current = gtk_list_box_get_selected_row(list); if (row != current) { gtk_list_box_select_row(list, row); } set_list_cursor(state, row != NULL); return FALSE; } static gboolean on_list_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data) { GtkListBox *list = GTK_LIST_BOX(widget); LauncherState *state = (LauncherState *)user_data; if (event->detail != GDK_NOTIFY_INFERIOR) { gtk_list_box_unselect_all(list); set_list_cursor(state, FALSE); } (void)event; return FALSE; } // nav: ui build static GtkWidget *create_entry_row(AppEntry *entry) { GtkWidget *row = gtk_list_box_row_new(); GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 26); GtkWidget *icon = NULL; GtkWidget *label = NULL; GIcon *gicon = g_app_info_get_icon(G_APP_INFO(entry->app)); gtk_widget_set_margin_start(box, 28); gtk_widget_set_margin_end(box, 28); gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16); if (gicon != NULL) { icon = gtk_image_new_from_gicon(gicon, GTK_ICON_SIZE_DIALOG); } else { icon = gtk_image_new_from_icon_name("application-x-executable", GTK_ICON_SIZE_DIALOG); } gtk_image_set_pixel_size(GTK_IMAGE(icon), 72); gtk_widget_set_halign(icon, GTK_ALIGN_START); gtk_widget_set_valign(icon, GTK_ALIGN_CENTER); label = gtk_label_new(entry->name); gtk_widget_set_name(label, "app-name"); gtk_label_set_xalign(GTK_LABEL(label), 0.0f); gtk_widget_set_halign(label, GTK_ALIGN_FILL); gtk_widget_set_valign(label, GTK_ALIGN_CENTER); gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0); gtk_container_add(GTK_CONTAINER(row), box); g_object_set_data(G_OBJECT(row), "entry", entry); return row; } static void apply_css(void) { const char *css = "window {" " background: #0f1219;" "}" "#header {" " color: #f3f7ff;" " font: 800 30px Sans;" " padding: 22px 30px 14px 30px;" "}" "list {" " background: transparent;" "}" "list row {" " background: transparent;" " border: 0;" "}" "list row:selected {" " background: #2e7bc7;" "}" "list row:hover {" " background: #2e7bc7;" "}" "#app-name {" " color: #f9fbff;" " font: 700 34px Sans;" "}" "#status {" " color: #ffb4b4;" " font: 500 18px Sans;" " padding: 12px 30px 22px 30px;" "}"; GtkCssProvider *provider = gtk_css_provider_new(); GdkScreen *screen = gdk_screen_get_default(); gtk_css_provider_load_from_data(provider, css, -1, NULL); if (screen != NULL) { gtk_style_context_add_provider_for_screen( screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION ); } g_object_unref(provider); } // nav: process setup static void ignore_nonfatal_signals(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGINT, &sa, NULL); sigaction(SIGHUP, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); } int main(int argc, char **argv) { LauncherState state; gboolean loaded_from_stdin = FALSE; gboolean loaded_from_config = FALSE; GtkWidget *root = NULL; GtkWidget *header = NULL; GtkWidget *scroller = NULL; guint i = 0; (void)argv; memset(&state, 0, sizeof(state)); state.entries = g_ptr_array_new_with_free_func(app_entry_free); state.seen_paths = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); if (state.entries == NULL || state.seen_paths == NULL) { g_printerr("failed to initialize launcher state\n"); launcher_state_cleanup(&state); return EXIT_FAILURE; } ignore_nonfatal_signals(); loaded_from_stdin = load_paths_from_stdin(&state); if (!loaded_from_stdin) { loaded_from_config = load_paths_from_config(&state); } if (!loaded_from_stdin && !loaded_from_config) { g_printerr("no desktop entries found from stdin or config\n"); launcher_state_cleanup(&state); return EXIT_FAILURE; } if (state.entries->len == 0) { g_printerr("no valid desktop entries were loaded\n"); launcher_state_cleanup(&state); return EXIT_FAILURE; } gtk_init(&argc, &argv); initialize_cursors(&state); apply_css(); state.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(state.window), "dapp launcher"); gtk_window_set_decorated(GTK_WINDOW(state.window), FALSE); gtk_window_set_deletable(GTK_WINDOW(state.window), FALSE); gtk_window_set_resizable(GTK_WINDOW(state.window), FALSE); gtk_window_set_skip_taskbar_hint(GTK_WINDOW(state.window), TRUE); gtk_window_set_skip_pager_hint(GTK_WINDOW(state.window), TRUE); gtk_window_fullscreen(GTK_WINDOW(state.window)); g_signal_connect(state.window, "delete-event", G_CALLBACK(on_delete_event), NULL); g_signal_connect(state.window, "key-press-event", G_CALLBACK(on_key_press), &state); root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); header = gtk_label_new("apps"); gtk_widget_set_name(header, "header"); gtk_label_set_xalign(GTK_LABEL(header), 0.0f); gtk_box_pack_start(GTK_BOX(root), header, FALSE, FALSE, 0); scroller = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_widget_set_vexpand(scroller, TRUE); gtk_widget_set_hexpand(scroller, TRUE); gtk_box_pack_start(GTK_BOX(root), scroller, TRUE, TRUE, 0); state.list = gtk_list_box_new(); gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(state.list), TRUE); gtk_list_box_set_selection_mode(GTK_LIST_BOX(state.list), GTK_SELECTION_SINGLE); gtk_widget_add_events(state.list, GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK); g_signal_connect(state.list, "row-activated", G_CALLBACK(on_row_activated), &state); g_signal_connect(state.list, "motion-notify-event", G_CALLBACK(on_list_motion), &state); g_signal_connect(state.list, "leave-notify-event", G_CALLBACK(on_list_leave), &state); for (i = 0; i < state.entries->len; i++) { AppEntry *entry = (AppEntry *)g_ptr_array_index(state.entries, i); GtkWidget *row = create_entry_row(entry); gtk_container_add(GTK_CONTAINER(state.list), row); } gtk_container_add(GTK_CONTAINER(scroller), state.list); state.status_label = gtk_label_new(""); gtk_widget_set_name(state.status_label, "status"); gtk_label_set_xalign(GTK_LABEL(state.status_label), 0.0f); gtk_box_pack_start(GTK_BOX(root), state.status_label, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(state.window), root); gtk_widget_show_all(state.window); set_list_cursor(&state, FALSE); gtk_main(); launcher_state_cleanup(&state); return EXIT_SUCCESS; }