diff options
| author | kj_sh604 | 2026-05-26 20:25:55 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-05-26 20:25:55 -0400 |
| commit | a6aa3c4c66199b48bdeadeaf23f86b8609be9db0 (patch) | |
| tree | dd1d6039957f1f9a843b3b20d2e62a2445cadb9a /src | |
| parent | 7231a3052a13c1642c399619b68f794e05d544e0 (diff) | |
feat: initial implementation
Diffstat (limited to 'src')
| -rw-r--r-- | src/dapp.c | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/src/dapp.c b/src/dapp.c new file mode 100644 index 0000000..6d7d651 --- /dev/null +++ b/src/dapp.c @@ -0,0 +1,531 @@ +#define _POSIX_C_SOURCE 200809L + +#include <gio/gdesktopappinfo.h> +#include <gtk/gtk.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +typedef struct { + char *desktop_path; + char *name; + GDesktopAppInfo *app; +} AppEntry; + +typedef struct { + GPtrArray *entries; + GtkWidget *window; + GtkWidget *list; + GtkWidget *status_label; +} LauncherState; + +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; +} + +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 gboolean add_desktop_path(LauncherState *state, const char *raw_path) { + char *path = NULL; + GDesktopAppInfo *app = NULL; + const char *display_name = NULL; + AppEntry *entry = NULL; + + path = expand_path(raw_path); + if (path == NULL) { + return FALSE; + } + + if (!g_file_test(path, G_FILE_TEST_IS_REGULAR)) { + g_free(path); + return FALSE; + } + + app = g_desktop_app_info_new_from_filename(path); + if (app == NULL) { + g_free(path); + return FALSE; + } + + display_name = g_app_info_get_display_name(G_APP_INFO(app)); + if (display_name == NULL || *display_name == '\0') { + display_name = g_app_info_get_name(G_APP_INFO(app)); + } + + entry = g_new0(AppEntry, 1); + entry->desktop_path = path; + entry->name = g_strdup(display_name != NULL ? display_name : raw_path); + entry->app = app; + + g_ptr_array_add(state->entries, entry); + return TRUE; +} + +static void load_paths_from_stream(LauncherState *state, FILE *stream, const char *source_name) { + char *line = NULL; + size_t line_cap = 0; + ssize_t line_len = 0; + guint line_no = 0; + + while ((line_len = getline(&line, &line_cap, stream)) != -1) { + char *trimmed = NULL; + line_no++; + + if (line_len > 0 && line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + } + + 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); + } + } + + free(line); +} + +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); + + fclose(config); + g_free(path); + return state->entries->len > old_len; +} + +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 : ""); +} + +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)); + } +} + +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); + GtkListBoxRow *row = NULL; + GtkListBoxRow *current = NULL; + + (void)user_data; + + 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); + } + + return FALSE; +} + +static gboolean on_list_leave(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data) { + GtkListBox *list = GTK_LIST_BOX(widget); + + (void)user_data; + + if (event->detail != GDK_NOTIFY_INFERIOR) { + gtk_list_box_unselect_all(list); + } + + (void)event; + return FALSE; +} + +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); +} + +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); + + 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"); + g_ptr_array_free(state.entries, TRUE); + return EXIT_FAILURE; + } + + if (state.entries->len == 0) { + g_printerr("no valid desktop entries were loaded\n"); + g_ptr_array_free(state.entries, TRUE); + return EXIT_FAILURE; + } + + gtk_init(&argc, &argv); + 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), NULL); + g_signal_connect(state.list, "leave-notify-event", G_CALLBACK(on_list_leave), NULL); + + 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); + + gtk_main(); + + g_ptr_array_free(state.entries, TRUE); + return EXIT_SUCCESS; +} |
