From c020b4228dec6b2e6489f58ee169aeb870a3da00 Mon Sep 17 00:00:00 2001 From: Shav Kinderlehrer Date: Fri, 8 May 2026 19:17:47 -0400 Subject: Add pretty border and implement lock Improves some of the scheme logic and adds a lock file so that people don't open a million instances of extant on accident. --- README | 2 +- data/resources.xml | 6 -- data/window.css | 77 +++++++++++++++-- flake.nix | 1 + main.c | 232 +++++++++++++++++++++++++++++++++++++++++++++++---- meson.build | 22 +++-- scm/extant-input.scm | 8 +- 7 files changed, 310 insertions(+), 38 deletions(-) delete mode 100644 data/resources.xml diff --git a/README b/README index 73537c1..90036d9 100644 --- a/README +++ b/README @@ -1 +1 @@ -EXTENdable assisTANT +EXTENsible assisTANT diff --git a/data/resources.xml b/data/resources.xml deleted file mode 100644 index 456a445..0000000 --- a/data/resources.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - window.css - - diff --git a/data/window.css b/data/window.css index 64357a3..4bfdba6 100644 --- a/data/window.css +++ b/data/window.css @@ -1,21 +1,88 @@ window { background: transparent; - border-radius: 999px; +} + +#messages-box { + padding: 15px; +} + +#messages-box .user, #messages-box .handler { + border: 1px solid @theme_fg_color; + padding: 5px; + margin: 5px 0; + border-radius: 15px; + caret-color: transparent; +} + +#messages-box .user { + background: @theme_bg_color; + border-bottom-right-radius: 3px; +} + +#messages-box .handler { + background: @theme_bg_color; + border-bottom-left-radius: 3px; } #prompt-box { - border: none; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border: 2px solid transparent; + border-radius: 12px; + padding: 3px; + margin: 0 20px; + + background: @theme_base_color; + animation: orbit 4s linear infinite; } #prompt-box button { border: none; - border-radius: 0px; + padding: 3px; + border-radius: 999px; + border: 1px solid @theme_fg_color; } #prompt-box entry { border: none; outline-width: 0px; - border-radius: 0px; + border-radius: 5px; } - +@keyframes orbit { + 0% { + box-shadow: + 5px 0 12px #3251a5, + 0 5px 12px #093d57, + -5px 0 12px #2b135e, + 0 -5px 12px #5455a6; + } + 25% { + box-shadow: + 0 5px 12px #3251a5, + -5px 0 12px #093d57, + 0 -5px 12px #2b135e, + 5px 0 12px #5455a6; + } + 50% { + box-shadow: + -5px 0 12px #3251a5, + 0 -5px 12px #093d57, + 5px 0 12px #2b135e, + 0 5px 12px #5455a6; + } + 75% { + box-shadow: + 0 -5px 12px #3251a5, + 5px 0 12px #093d57, + 0 5px 12px #2b135e, + -5px 0 12px #5455a6; + } + 100% { + box-shadow: + 5px 0 12px #3251a5, + 0 5px 12px #093d57, + -5px 0 12px #2b135e, + 0 -5px 12px #5455a6; + } +} diff --git a/flake.nix b/flake.nix index 1450caf..c35b3c1 100644 --- a/flake.nix +++ b/flake.nix @@ -18,6 +18,7 @@ meson ninja guile + gdb ]; }; } diff --git a/main.c b/main.c index 0d71046..449f994 100644 --- a/main.c +++ b/main.c @@ -1,24 +1,132 @@ -#include "gdk/gdk.h" -#include "gio/gio.h" -#include "glib-object.h" -#include "gtk/gtkcssprovider.h" +#include +#include +#include +#include #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct window_widgets { + GtkWidget *entry; + GtkWidget *button; + GtkWidget *responses_window; + GtkWidget *responses_box; +}; + +static void input_widgets_clear(struct window_widgets *widgets) { + gtk_editable_set_text(GTK_EDITABLE(widgets->entry), ""); +} static void setup_window_styles(GtkWindow *window) { GtkCssProvider *css_provider = gtk_css_provider_new(); gtk_css_provider_load_from_resource(css_provider, - "/ski/frog/assistant/window.css"); + "/ski/frog/extant/styles/window.css"); gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); } -static gboolean on_escape_key_pressed(GtkEventControllerKey *controller, guint keyval, - guint keycode, GdkModifierType state, - gpointer user_data) { +static void guile_load_bundled_guile() { + GError *error = NULL; + GBytes *data = + g_resources_lookup_data("/ski/frog/extant/guile/extant-input.scm", + G_RESOURCE_LOOKUP_FLAGS_NONE, &error); + + if (error) { + g_warning("Failed to load extant input: %s", error->message); + g_error_free(error); + return; + } + + gsize size; + const gchar *script_contents = g_bytes_get_data(data, &size); + char *null_terminated_script_contents = g_strndup(script_contents, size); + scm_c_eval_string(null_terminated_script_contents); + g_free(null_terminated_script_contents); + g_bytes_unref(data); +} + +static void guile_load_user_config() { + char *user_config = + g_build_filename(g_get_user_config_dir(), "extant", "init.scm", NULL); + if (g_file_test(user_config, G_FILE_TEST_EXISTS)) { + scm_c_primitive_load(user_config); + } else { + g_warning("User config %s not found.", user_config); + } + g_free(user_config); +} + +static SCM guile_load_input_module() { + SCM module_name = + scm_list_2(scm_from_utf8_symbol("extant"), scm_from_utf8_symbol("input")); + SCM module = scm_resolve_module(module_name); + return module; +} + +static void guile_setup() { + scm_init_guile(); + guile_load_bundled_guile(); + guile_load_user_config(); +} + +GtkWidget *create_message(const char *message) { + GtkWidget *label = gtk_label_new(message); + gtk_label_set_max_width_chars(GTK_LABEL(label), 20); + gtk_label_set_wrap(GTK_LABEL(label), TRUE); + gtk_label_set_selectable(GTK_LABEL(label), TRUE); + + return label; +} + +GtkWidget *create_user_message(const char *message) { + GtkWidget *label = create_message(message); + gtk_widget_add_css_class(label, "user"); + gtk_widget_set_halign(label, GTK_ALIGN_END); + + return label; +} + +GtkWidget *create_handler_message(const char *message) { + GtkWidget *label = create_message(message); + gtk_widget_add_css_class(label, "handler"); + gtk_widget_set_halign(label, GTK_ALIGN_START); + + return label; +} + +static gboolean scroll_to_bottom(gpointer data) { + GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW(data); + GtkAdjustment *vertical_adjustment = + gtk_scrolled_window_get_vadjustment(scrolled_window); + double bottom = gtk_adjustment_get_upper(vertical_adjustment) - + gtk_adjustment_get_page_size(vertical_adjustment); + gtk_adjustment_set_value(vertical_adjustment, bottom); + return G_SOURCE_REMOVE; +} + +static gboolean on_focus_leave(GtkEventControllerFocus *controller, + gpointer data) { + GtkWindow *window = GTK_WINDOW(data); + gtk_window_close(window); + return FALSE; +} + +static gboolean on_escape_key_pressed(GtkEventControllerKey *controller, + guint keyval, guint keycode, + GdkModifierType state, + gpointer user_data) { GtkWindow *window = GTK_WINDOW(user_data); if (keyval == GDK_KEY_Escape) { gtk_window_close(window); @@ -34,42 +142,134 @@ static void setup_close_on_escape(GtkWindow *window) { gtk_widget_add_controller(GTK_WIDGET(window), controller); } -static void on_submit(GtkWidget *widget, gpointer data) { - GtkEntry *entry = GTK_ENTRY(data); - const char *text = gtk_editable_get_text(GTK_EDITABLE(entry)); - g_print("Got: %s\n", text); +static gboolean on_submit(GtkWidget *widget, gpointer data) { + struct window_widgets *widgets = (struct window_widgets *)data; + const char *text = gtk_editable_get_text(GTK_EDITABLE(widgets->entry)); + if (strlen(text) == 0) { + return FALSE; + } + + gtk_box_append(GTK_BOX(widgets->responses_box), create_user_message(text)); + g_idle_add(scroll_to_bottom, widgets->responses_window); + + SCM module = guile_load_input_module(); + SCM dispatch_var = + scm_module_variable(module, scm_from_utf8_symbol("dispatch-input")); + if (scm_is_true(dispatch_var)) { + SCM dispatch_func = scm_variable_ref(dispatch_var); + SCM result = scm_call_1(dispatch_func, scm_from_utf8_string(text)); + + SCM text_result = scm_assoc_ref(result, scm_from_utf8_symbol("text")); + if (scm_is_string(text_result)) { + const char *reply = scm_to_utf8_string(text_result); + gtk_box_append(GTK_BOX(widgets->responses_box), + create_handler_message(reply)); + g_idle_add(scroll_to_bottom, widgets->responses_window); + + free((char *)reply); + } else { + g_warning("Guile did not reply with a valid response!"); + } + } else { + g_warning("Could not dispatch input to guile!"); + } + + input_widgets_clear(widgets); + return FALSE; } static void activate(GtkApplication *app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); + /* + GtkEventController *focus_controller = gtk_event_controller_focus_new(); + g_signal_connect(focus_controller, "leave", G_CALLBACK(on_focus_leave), + window); + gtk_widget_add_controller(window, focus_controller); + */ gtk_layer_init_for_window(GTK_WINDOW(window)); - gtk_layer_set_layer(GTK_WINDOW(window), GTK_LAYER_SHELL_LAYER_OVERLAY); + gtk_layer_set_layer(GTK_WINDOW(window), GTK_LAYER_SHELL_LAYER_TOP); gtk_layer_set_keyboard_mode(GTK_WINDOW(window), GTK_LAYER_SHELL_KEYBOARD_MODE_EXCLUSIVE); + gtk_layer_set_anchor(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_TOP, TRUE); + gtk_layer_set_anchor(GTK_WINDOW(window), GTK_LAYER_SHELL_EDGE_BOTTOM, TRUE); gtk_window_set_decorated(GTK_WINDOW(window), FALSE); setup_window_styles(GTK_WINDOW(window)); setup_close_on_escape(GTK_WINDOW(window)); + GtkWidget *window_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(window_container, TRUE); + + GtkWidget *top_overlay = gtk_overlay_new(); + gtk_widget_set_vexpand(top_overlay, TRUE); + + GtkWidget *top_spacer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_overlay_set_child(GTK_OVERLAY(top_overlay), top_spacer); + + GtkWidget *bottom_half = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_vexpand(bottom_half, TRUE); + + GtkWidget *messages_window = gtk_scrolled_window_new(); + gtk_scrolled_window_set_propagate_natural_height( + GTK_SCROLLED_WINDOW(messages_window), TRUE); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(messages_window), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + + gtk_overlay_add_overlay(GTK_OVERLAY(top_overlay), messages_window); + gtk_widget_set_valign(messages_window, GTK_ALIGN_END); + + GtkWidget *messages_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); + gtk_widget_set_name(messages_box, "messages-box"); + gtk_widget_set_valign(messages_box, GTK_ALIGN_END); + + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(messages_window), + messages_box); + GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gtk_widget_set_name(hbox, "prompt-box"); + gtk_widget_set_overflow(hbox, GTK_OVERFLOW_HIDDEN); GtkWidget *entry = gtk_entry_new(); + gtk_editable_set_width_chars(GTK_EDITABLE(entry), 50); + GtkWidget *submit_button = gtk_button_new_from_icon_name("send-to-symbolic"); - g_signal_connect(submit_button, "clicked", G_CALLBACK(on_submit), entry); - g_signal_connect(entry, "activate", G_CALLBACK(on_submit), entry); + struct window_widgets *widgets = g_new(struct window_widgets, 1); + widgets->responses_box = messages_box; + widgets->entry = entry; + widgets->button = submit_button; + widgets->responses_window = messages_window; + + g_signal_connect(submit_button, "clicked", G_CALLBACK(on_submit), widgets); + g_signal_connect(entry, "activate", G_CALLBACK(on_submit), widgets); gtk_box_append(GTK_BOX(hbox), entry); gtk_box_append(GTK_BOX(hbox), submit_button); - gtk_window_set_child(GTK_WINDOW(window), hbox); + gtk_box_append(GTK_BOX(window_container), top_overlay); + gtk_box_append(GTK_BOX(window_container), hbox); + gtk_box_append(GTK_BOX(window_container), bottom_half); + + gtk_window_set_child(GTK_WINDOW(window), window_container); + gtk_widget_grab_focus(entry); gtk_window_present(GTK_WINDOW(window)); } int main(int argc, char *argv[]) { + int pid_file = open("/tmp/extant.lock", O_CREAT | O_RDWR, 0666); + if (flock(pid_file, LOCK_EX | LOCK_NB) != 0) { + if (errno == EWOULDBLOCK) { + fprintf(stderr, "Another instance is already running.\n"); + return 1; + } else { + perror("Unable to acquire lock"); + return 1; + } + } + + guile_setup(); GtkApplication *app = gtk_application_new("ski.frog.assistant", G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); diff --git a/meson.build b/meson.build index b8c1d8f..cbdcbbf 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('assistant', 'c') +project('extant', 'c') gtk = dependency('gtk4') glib = dependency('glib-2.0') @@ -7,17 +7,25 @@ libguile = dependency('guile-3.0') gnome = import('gnome') -resources = gnome.compile_resources( - 'resources', - 'data/resources.xml', +styles = gnome.compile_resources( + 'styles', + 'data/styles.xml', source_dir: 'data', - c_name: 'resources' + c_name: 'styles' +) + +guile = gnome.compile_resources( + 'guile', + 'scm/guile.xml', + source_dir: 'scm', + c_name: 'guile' ) executable( - 'assistant', + 'extant', 'main.c', - resources, + styles, + guile, dependencies: [ gtk, glib, diff --git a/scm/extant-input.scm b/scm/extant-input.scm index d50a7fe..b06aea6 100644 --- a/scm/extant-input.scm +++ b/scm/extant-input.scm @@ -9,7 +9,9 @@ (define (dispatch-input text) (let loop ((handlers *handlers*)) (if (null? handlers) - `((text . "No handlers found!") - (style . "error")) + '((text . "I could not find a suitable handler for this message.") + (style . error)) (let ((result ((car handlers) text))) - (or result (loop (cdr handlers))))))) + (if (null? result) + (loop (cdr handlers)) + result))))) -- cgit v1.2.3