aboutsummaryrefslogtreecommitdiff
path: root/main.c
diff options
context:
space:
mode:
authorShav Kinderlehrer <shav@trinket.icu>2026-05-08 19:17:47 -0400
committerShav Kinderlehrer <shav@trinket.icu>2026-05-08 19:17:47 -0400
commitc020b4228dec6b2e6489f58ee169aeb870a3da00 (patch)
tree6f83985862b91ee6e4bd9865280f3beaa1bed4a3 /main.c
parent91e1dbfd946f8396709d83701a6651b5c2ca7bdc (diff)
downloadextant-main.tar.gz
extant-main.zip
Add pretty border and implement lockHEADmain
Improves some of the scheme logic and adds a lock file so that people don't open a million instances of extant on accident.
Diffstat (limited to 'main.c')
-rw-r--r--main.c232
1 files changed, 216 insertions, 16 deletions
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);