aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorkj_sh6042026-05-26 20:25:55 -0400
committerkj_sh6042026-05-26 20:25:55 -0400
commita6aa3c4c66199b48bdeadeaf23f86b8609be9db0 (patch)
treedd1d6039957f1f9a843b3b20d2e62a2445cadb9a /src
parent7231a3052a13c1642c399619b68f794e05d544e0 (diff)
feat: initial implementation
Diffstat (limited to 'src')
-rw-r--r--src/dapp.c531
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;
+}