// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "native.h" #include #include enum { NONE, COPY, REMOVE, CLOSE, CLEAR }; static GtkWidget *window, *filter, *tree, *text_view; static GtkListStore *store; static GtkTreeModel *filtered; static GtkTreeSelection *selection; static int action_code, monitor_mode, line_count; static char *selected_cache; static void action(GtkButton *button, gpointer code) { (void)button; if (!action_code) action_code = GPOINTER_TO_INT(code); } static gboolean deleted(GtkWidget *widget, GdkEvent *event, gpointer data) { (void)widget; (void)event; (void)data; if (!action_code) action_code = CLOSE; return TRUE; } static gboolean key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) { (void)widget; (void)data; if (event->keyval == GDK_KEY_Escape) { action_code = CLOSE; return TRUE; } if (monitor_mode || !filter || !gtk_widget_has_focus(tree)) return FALSE; if (event->keyval == GDK_KEY_BackSpace) { const char *current = gtk_entry_get_text(GTK_ENTRY(filter)); glong length = g_utf8_strlen(current, -1); if (length > 0) { char *shorter = g_utf8_substring(current, 0, length - 1); gtk_entry_set_text(GTK_ENTRY(filter), shorter); gtk_editable_set_position(GTK_EDITABLE(filter), -1); g_free(shorter); return TRUE; } return FALSE; } if (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_SUPER_MASK | GDK_META_MASK)) return FALSE; gunichar ch = gdk_keyval_to_unicode(event->keyval); if (!ch || !g_unichar_isprint(ch)) return FALSE; char utf8[7] = {0}; g_unichar_to_utf8(ch, utf8); char *updated = g_strconcat(gtk_entry_get_text(GTK_ENTRY(filter)), utf8, NULL); gtk_entry_set_text(GTK_ENTRY(filter), updated); gtk_editable_set_position(GTK_EDITABLE(filter), -1); gtk_widget_grab_focus(filter); g_free(updated); return TRUE; } static gboolean visible(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) { (void)data; char *query = g_strstrip(g_strdup(gtk_entry_get_text(GTK_ENTRY(filter)))); if (!query[0]) { g_free(query); return TRUE; } char *value = NULL; gtk_tree_model_get(model, iter, 0, &value, -1); char *folded_value = g_utf8_casefold(value ? value : "", -1); char *folded_query = g_utf8_casefold(query, -1); gboolean match = g_str_has_prefix(folded_value, folded_query); g_free(value); g_free(query); g_free(folded_value); g_free(folded_query); return match; } static void filter_changed(GtkEditable *editable, gpointer data) { (void)editable; (void)data; gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(filtered)); GtkTreeIter first; if (gtk_tree_model_get_iter_first(filtered, &first)) { GtkTreePath *path = gtk_tree_model_get_path(filtered, &first); gtk_tree_selection_select_path(selection, path); gtk_tree_path_free(path); } else gtk_tree_selection_unselect_all(selection); } static GtkWidget *button(GtkWidget *box, const char *label, int code) { GtkWidget *widget = gtk_button_new_with_label(label); g_signal_connect(widget, "clicked", G_CALLBACK(action), GINT_TO_POINTER(code)); gtk_box_pack_start(GTK_BOX(box), widget, FALSE, FALSE, 0); return widget; } int history_init(int monitor) { if (!gtk_init_check(NULL, NULL)) return 0; monitor_mode = monitor; line_count = 0; g_set_application_name(monitor ? "Cthulhu Speech Monitor" : "Cthulhu Speech History"); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), monitor ? "Speech Monitor - Cthulhu" : "Speech History - Cthulhu"); gtk_window_set_default_size(GTK_WINDOW(window), monitor ? 900 : 700, monitor ? 320 : 420); gtk_container_set_border_width(GTK_CONTAINER(window), 10); if (monitor) { gtk_window_set_keep_above(GTK_WINDOW(window), TRUE); gtk_window_set_focus_on_map(GTK_WINDOW(window), FALSE); gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_UTILITY); } else gtk_window_set_modal(GTK_WINDOW(window), TRUE); g_signal_connect(window, "delete-event", G_CALLBACK(deleted), NULL); g_signal_connect(window, "key-press-event", G_CALLBACK(key_press), NULL); GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); gtk_container_add(GTK_CONTAINER(window), box); if (!monitor) { GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); GtkWidget *label = gtk_label_new_with_mnemonic("_Filter:"); filter = gtk_entry_new(); gtk_widget_set_hexpand(filter, TRUE); gtk_label_set_mnemonic_widget(GTK_LABEL(label), filter); gtk_box_pack_start(GTK_BOX(row), label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(row), filter, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0); store = gtk_list_store_new(1, G_TYPE_STRING); filtered = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL); gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filtered), visible, NULL, NULL); g_signal_connect(filter, "changed", G_CALLBACK(filter_changed), NULL); } GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); gtk_widget_set_vexpand(scroll, TRUE); gtk_box_pack_start(GTK_BOX(box), scroll, TRUE, TRUE, 0); if (monitor) { text_view = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(text_view), FALSE); gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text_view), FALSE); gtk_widget_set_can_focus(text_view, FALSE); gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view), GTK_WRAP_WORD_CHAR); AtkObject *accessible = gtk_widget_get_accessible(text_view); if (accessible) atk_object_set_name(accessible, "Speech monitor output"); gtk_container_add(GTK_CONTAINER(scroll), text_view); } else { tree = gtk_tree_view_new_with_model(filtered); GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); g_object_set(renderer, "wrap-width", 640, "wrap-mode", PANGO_WRAP_WORD_CHAR, NULL); GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Spoken Text", renderer, "text", 0, NULL); gtk_tree_view_column_set_expand(column, TRUE); gtk_tree_view_column_set_resizable(column, TRUE); gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column); selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); gtk_container_add(GTK_CONTAINER(scroll), tree); } GtkWidget *buttons = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(buttons, GTK_ALIGN_END); gtk_box_pack_start(GTK_BOX(box), buttons, FALSE, FALSE, 0); if (monitor) button(buttons, "Clear", CLEAR); else { button(buttons, "Copy to clipboard", COPY); button(buttons, "Remove from history", REMOVE); } button(buttons, "Close", CLOSE); gtk_widget_show_all(window); if (!monitor) gtk_widget_grab_focus(filter); return 1; } void history_set_items(char **items, int count, int selected) { if (!store) return; gtk_list_store_clear(store); for (int i = 0; i < count; i++) { GtkTreeIter iter; gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, items[i], -1); } if (count > 0) { GtkTreePath *path = gtk_tree_path_new_from_indices(selected >= 0 && selected < count ? selected : 0, -1); gtk_tree_selection_select_path(selection, path); gtk_tree_path_free(path); gtk_widget_grab_focus(tree); } else gtk_widget_grab_focus(filter); } void history_append(const char *text) { if (!text_view || !text) return; GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)); GtkTextIter end; gtk_text_buffer_get_end_iter(buffer, &end); gtk_text_buffer_insert(buffer, &end, text, -1); gtk_text_buffer_insert(buffer, &end, "\n", 1); if (++line_count > 500) { GtkTextIter start, cut; gtk_text_buffer_get_start_iter(buffer, &start); gtk_text_buffer_get_iter_at_line(buffer, &cut, line_count - 500); gtk_text_buffer_delete(buffer, &start, &cut); line_count = 500; } gtk_text_buffer_get_end_iter(buffer, &end); GtkTextMark *mark = gtk_text_buffer_create_mark(buffer, NULL, &end, FALSE); gtk_text_view_scroll_mark_onscreen(GTK_TEXT_VIEW(text_view), mark); gtk_text_buffer_delete_mark(buffer, mark); } void history_present(void) { if (window) gtk_window_present(GTK_WINDOW(window)); } void history_iterate(void) { while (gtk_events_pending()) gtk_main_iteration(); } int history_action(void) { int result = action_code; action_code = NONE; return result; } const char *history_selected(void) { GtkTreeIter iter; GtkTreeModel *model = NULL; if (!selection || !gtk_tree_selection_get_selected(selection, &model, &iter)) return ""; g_free(selected_cache); gtk_tree_model_get(model, &iter, 0, &selected_cache, -1); return selected_cache ? selected_cache : ""; } int history_selected_row(void) { GtkTreeIter iter; GtkTreeModel *model = NULL; if (!selection || !gtk_tree_selection_get_selected(selection, &model, &iter)) return 0; GtkTreePath *path = gtk_tree_model_get_path(model, &iter); int *indices = gtk_tree_path_get_indices(path); int row = indices ? indices[0] : 0; gtk_tree_path_free(path); return row; } int history_copy(void) { const char *text = history_selected(); if (!text || !text[0]) return 0; GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_set_text(clipboard, text, -1); gtk_clipboard_store(clipboard); return 1; } void history_clear(void) { if (!text_view) return; gtk_text_buffer_set_text(gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view)), "", 0); line_count = 0; } void history_close(void) { if (window) gtk_widget_destroy(window); window = filter = tree = text_view = NULL; store = NULL; filtered = NULL; selection = NULL; g_free(selected_cache); selected_cache = NULL; }