#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/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 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); return TRUE; } return FALSE; } static void setup_close_on_escape(GtkWindow *window) { GtkEventController *controller = gtk_event_controller_key_new(); g_signal_connect(controller, "key-pressed", G_CALLBACK(on_escape_key_pressed), window); gtk_widget_add_controller(GTK_WIDGET(window), controller); } 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_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"); 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_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); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }