aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README2
-rw-r--r--data/resources.xml6
-rw-r--r--data/window.css77
-rw-r--r--flake.nix1
-rw-r--r--main.c232
-rw-r--r--meson.build22
-rw-r--r--scm/extant-input.scm8
7 files changed, 310 insertions, 38 deletions
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 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<gresources>
- <gresource prefix="/ski/frog/assistant">
- <file>window.css</file>
- </gresource>
-</gresources>
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 <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <glib.h>
#include <gtk/gtk.h>
+#include <gtk/gtkcssprovider.h>
#include <gtk4-layer-shell/gtk4-layer-shell.h>
#include <libguile.h>
+#include <libguile/init.h>
+#include <libguile/modules.h>
+#include <libguile/strings.h>
+#include <libguile/symbols.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include <stdio.h>
+
+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)))))