Rework application to support GTK4 GUI by default and TUI via -t flag
Some checks failed
Build / build (push) Failing after 4s

This commit is contained in:
2026-06-07 18:02:33 +02:00
parent 873ae5ae9d
commit d7ff90d072
4 changed files with 599 additions and 82 deletions

482
gui.c Normal file
View File

@@ -0,0 +1,482 @@
#include <gtk/gtk.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "gui.h"
// Define CSS data for the application's look and feel
static const char* css_data =
"window {\n"
" background-color: #0b090a;\n"
" color: #f5f3f4;\n"
" font-family: 'Inter', 'Segoe UI', sans-serif;\n"
"}\n"
"\n"
".title-label {\n"
" font-size: 28px;\n"
" font-weight: 800;\n"
" color: #f5f3f4;\n"
" margin-top: 15px;\n"
" margin-bottom: 5px;\n"
" letter-spacing: 2px;\n"
"}\n"
"\n"
".subtitle-label {\n"
" font-size: 14px;\n"
" color: #b1a7a6;\n"
" margin-bottom: 15px;\n"
"}\n"
"\n"
".worry-text-view {\n"
" font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;\n"
" font-size: 16px;\n"
" background-color: #161a1d;\n"
" color: #f5f3f4;\n"
" border: 2px solid #3f3f46;\n"
" border-radius: 8px;\n"
" padding: 15px;\n"
"}\n"
"\n"
".worry-text-view:focus {\n"
" border-color: #7209b7;\n"
"}\n"
"\n"
".action-button {\n"
" font-size: 14px;\n"
" font-weight: 600;\n"
" padding: 12px 24px;\n"
" border-radius: 8px;\n"
" border: none;\n"
" color: white;\n"
" margin-bottom: 10px;\n"
"}\n"
"\n"
".btn-scream {\n"
" background: #7209b7;\n"
" box-shadow: 0 4px 12px rgba(114, 9, 183, 0.3);\n"
"}\n"
"\n"
".btn-scream:hover {\n"
" background: #8f20d8;\n"
"}\n"
"\n"
".btn-burn {\n"
" background: #d90429;\n"
" box-shadow: 0 4px 12px rgba(217, 4, 41, 0.3);\n"
"}\n"
"\n"
".btn-burn:hover {\n"
" background: #ef233c;\n"
"}\n"
"\n"
".btn-dissolve {\n"
" background: #06d6a0;\n"
" color: #0b090a;\n"
" box-shadow: 0 4px 12px rgba(6, 214, 160, 0.3);\n"
"}\n"
"\n"
".btn-dissolve:hover {\n"
" background: #1ae8b2;\n"
"}\n"
"\n"
".text-scream {\n"
" color: #b388ff;\n"
"}\n"
"\n"
".text-burn {\n"
" color: #ff9100;\n"
"}\n"
"\n"
".text-dissolve {\n"
" color: #00e5ff;\n"
"}\n"
"\n"
".consumed-label {\n"
" font-size: 24px;\n"
" font-weight: 700;\n"
" color: #f5f3f4;\n"
" margin-bottom: 25px;\n"
"}\n"
"\n"
".btn-reset {\n"
" background-color: #3f3f46;\n"
" color: white;\n"
" padding: 10px 20px;\n"
" border-radius: 6px;\n"
" border: 1px solid #52525b;\n"
"}\n"
"\n"
".btn-reset:hover {\n"
" background-color: #52525b;\n"
"}\n"
;
typedef enum {
ANIM_SCREAM,
ANIM_BURN,
ANIM_DISSOLVE
} AnimType;
typedef struct {
GtkWidget *stack;
GtkWidget *text_view; // Input text view
GtkWidget *anim_text_view; // Animation text view (read-only)
GtkWidget *anim_title; // Title during animation
GtkWidget *consumed_label; // Label on the consumed page
AnimType type;
char *original_text;
char *animated_text;
int step;
int max_steps;
guint timer_id;
} AppState;
static AppState app_state = {NULL, NULL, NULL, NULL, NULL, ANIM_SCREAM, NULL, NULL, 0, 0, 0};
// Forward declarations
static gboolean anim_timer_tick(gpointer user_data);
static void start_animation(AnimType type, const char* title, int max_steps, int interval_ms);
static void on_btn_scream_clicked(GtkButton *btn, gpointer user_data);
static void on_btn_burn_clicked(GtkButton *btn, gpointer user_data);
static void on_btn_dissolve_clicked(GtkButton *btn, gpointer user_data);
static void on_btn_reset_clicked(GtkButton *btn, gpointer user_data);
static gboolean anim_timer_tick(gpointer user_data) {
AppState *state = (AppState*)user_data;
if (state->animated_text == NULL) {
state->timer_id = 0;
return FALSE;
}
int len = strlen(state->animated_text);
if (state->type == ANIM_SCREAM) {
for (int i = 0; i < len; i++) {
if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n' && state->animated_text[i] != '.' && (rand() % 100) < (state->step * 7)) {
state->animated_text[i] = (rand() % 2 == 0) ? '.' : ' ';
}
}
} else if (state->type == ANIM_BURN) {
const char* fire_chars = "@#%*+=-:. ";
int fire_len = strlen(fire_chars);
for (int i = 0; i < len; i++) {
if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n') {
int r = rand() % 100;
if (r < (state->step * 10)) {
int char_idx = (state->step / 3) + (rand() % 3);
if (char_idx >= fire_len) state->animated_text[i] = ' ';
else state->animated_text[i] = fire_chars[char_idx];
}
}
}
} else if (state->type == ANIM_DISSOLVE) {
for (int i = 0; i < len; i++) {
if (state->animated_text[i] != ' ' && state->animated_text[i] != '\n') {
if ((rand() % 100) < (state->step * 5)) {
state->animated_text[i] = (rand() % 5 == 0) ? '~' : (rand() % 2 == 0 ? ',' : ' ');
}
}
}
}
// Update the animation buffer
GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(state->anim_text_view));
gtk_text_buffer_set_text(buf, state->animated_text, -1);
state->step++;
if (state->step > state->max_steps) {
state->timer_id = 0;
// Transition to consumed page
gtk_stack_set_visible_child_name(GTK_STACK(state->stack), "consumed");
if (state->type == ANIM_SCREAM) {
gtk_label_set_text(GTK_LABEL(state->consumed_label), "The void has consumed your worries.");
} else if (state->type == ANIM_BURN) {
gtk_label_set_text(GTK_LABEL(state->consumed_label), "Ashes to ashes. Dust to dust.");
} else {
gtk_label_set_text(GTK_LABEL(state->consumed_label), "Dissolved into nothingness.");
}
if (state->original_text) {
g_free(state->original_text);
state->original_text = NULL;
}
if (state->animated_text) {
g_free(state->animated_text);
state->animated_text = NULL;
}
return FALSE; // Stop the timer
}
return TRUE; // Continue the timer
}
static void start_animation(AnimType type, const char* title, int max_steps, int interval_ms) {
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view));
GtkTextIter start, end;
gtk_text_buffer_get_bounds(buffer, &start, &end);
char *text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
if (text == NULL || strlen(text) == 0) {
g_free(text);
return;
}
// Clean up any old timer or strings
if (app_state.timer_id != 0) {
g_source_remove(app_state.timer_id);
app_state.timer_id = 0;
}
if (app_state.original_text) {
g_free(app_state.original_text);
app_state.original_text = NULL;
}
if (app_state.animated_text) {
g_free(app_state.animated_text);
app_state.animated_text = NULL;
}
app_state.type = type;
app_state.original_text = g_strdup(text);
app_state.animated_text = g_strdup(text);
app_state.step = 0;
app_state.max_steps = max_steps;
g_free(text);
// Set up css classes for anim text view
gtk_widget_remove_css_class(app_state.anim_text_view, "text-scream");
gtk_widget_remove_css_class(app_state.anim_text_view, "text-burn");
gtk_widget_remove_css_class(app_state.anim_text_view, "text-dissolve");
if (type == ANIM_SCREAM) {
gtk_widget_add_css_class(app_state.anim_text_view, "text-scream");
} else if (type == ANIM_BURN) {
gtk_widget_add_css_class(app_state.anim_text_view, "text-burn");
} else {
gtk_widget_add_css_class(app_state.anim_text_view, "text-dissolve");
}
gtk_label_set_text(GTK_LABEL(app_state.anim_title), title);
// Set initial text on anim view
GtkTextBuffer *anim_buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.anim_text_view));
gtk_text_buffer_set_text(anim_buf, app_state.animated_text, -1);
// Transition to animation screen
gtk_stack_set_visible_child_name(GTK_STACK(app_state.stack), "anim");
// Start timer
app_state.timer_id = g_timeout_add(interval_ms, anim_timer_tick, &app_state);
}
static void on_btn_scream_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
(void)user_data;
start_animation(ANIM_SCREAM, "SCREAMING INTO THE VOID...", 20, 150);
}
static void on_btn_burn_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
(void)user_data;
start_animation(ANIM_BURN, "LETTING IT BURN...", 25, 100);
}
static void on_btn_dissolve_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
(void)user_data;
start_animation(ANIM_DISSOLVE, "LETTING IT DISSOLVE...", 30, 100);
}
static void on_btn_reset_clicked(GtkButton *btn, gpointer user_data) {
(void)btn;
(void)user_data;
GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view));
gtk_text_buffer_set_text(buf, "", -1);
gtk_stack_set_visible_child_name(GTK_STACK(app_state.stack), "input");
gtk_widget_grab_focus(app_state.text_view);
}
static void activate(GtkApplication* app, gpointer user_data) {
const char* initial_text = (const char*)user_data;
// Create main window
GtkWidget *window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), "Scream Into The Void");
gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
// Apply CSS styling
GtkCssProvider *provider = gtk_css_provider_new();
gtk_css_provider_load_from_string(provider, css_data);
gtk_style_context_add_provider_for_display(
gdk_display_get_default(),
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
g_object_unref(provider);
// Main layout container: Stack
app_state.stack = gtk_stack_new();
gtk_stack_set_transition_type(GTK_STACK(app_state.stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE);
gtk_stack_set_transition_duration(GTK_STACK(app_state.stack), 400);
gtk_window_set_child(GTK_WINDOW(window), app_state.stack);
// ----------------------------------------------------
// PAGE 1: Input Page
// ----------------------------------------------------
GtkWidget *input_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(input_page, 40);
gtk_widget_set_margin_end(input_page, 40);
gtk_widget_set_margin_top(input_page, 20);
gtk_widget_set_margin_bottom(input_page, 30);
GtkWidget *title_label = gtk_label_new("SCREAM INTO THE VOID");
gtk_widget_add_css_class(title_label, "title-label");
gtk_box_append(GTK_BOX(input_page), title_label);
GtkWidget *subtitle_label = gtk_label_new("What's weighing on your soul?");
gtk_widget_add_css_class(subtitle_label, "subtitle-label");
gtk_box_append(GTK_BOX(input_page), subtitle_label);
// Scrolled text area for input
GtkWidget *scrolled_window = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scrolled_window, TRUE);
gtk_widget_set_hexpand(scrolled_window, TRUE);
gtk_widget_set_margin_top(scrolled_window, 10);
gtk_widget_set_margin_bottom(scrolled_window, 15);
app_state.text_view = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(app_state.text_view), GTK_WRAP_WORD_CHAR);
gtk_widget_add_css_class(app_state.text_view, "worry-text-view");
// Set initial text if any (e.g. read from file)
if (initial_text && strlen(initial_text) > 0) {
GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(app_state.text_view));
gtk_text_buffer_set_text(buf, initial_text, -1);
}
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled_window), app_state.text_view);
gtk_box_append(GTK_BOX(input_page), scrolled_window);
// Action buttons container
GtkWidget *btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 20);
gtk_widget_set_halign(btn_box, GTK_ALIGN_CENTER);
GtkWidget *btn_scream = gtk_button_new_with_label("Scream it into the void");
gtk_widget_add_css_class(btn_scream, "action-button");
gtk_widget_add_css_class(btn_scream, "btn-scream");
g_signal_connect(btn_scream, "clicked", G_CALLBACK(on_btn_scream_clicked), NULL);
GtkWidget *btn_burn = gtk_button_new_with_label("Let it burn");
gtk_widget_add_css_class(btn_burn, "action-button");
gtk_widget_add_css_class(btn_burn, "btn-burn");
g_signal_connect(btn_burn, "clicked", G_CALLBACK(on_btn_burn_clicked), NULL);
GtkWidget *btn_dissolve = gtk_button_new_with_label("Let it dissolve");
gtk_widget_add_css_class(btn_dissolve, "action-button");
gtk_widget_add_css_class(btn_dissolve, "btn-dissolve");
g_signal_connect(btn_dissolve, "clicked", G_CALLBACK(on_btn_dissolve_clicked), NULL);
gtk_box_append(GTK_BOX(btn_box), btn_scream);
gtk_box_append(GTK_BOX(btn_box), btn_burn);
gtk_box_append(GTK_BOX(btn_box), btn_dissolve);
gtk_box_append(GTK_BOX(input_page), btn_box);
gtk_stack_add_named(GTK_STACK(app_state.stack), input_page, "input");
// ----------------------------------------------------
// PAGE 2: Animation Page
// ----------------------------------------------------
GtkWidget *anim_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(anim_page, 40);
gtk_widget_set_margin_end(anim_page, 40);
gtk_widget_set_margin_top(anim_page, 20);
gtk_widget_set_margin_bottom(anim_page, 30);
app_state.anim_title = gtk_label_new("THE VOID IS CONSUMING...");
gtk_widget_add_css_class(app_state.anim_title, "title-label");
gtk_box_append(GTK_BOX(anim_page), app_state.anim_title);
// Subtitle placeholder to keep consistent alignment
GtkWidget *anim_subtitle = gtk_label_new("Release your hold on these thoughts.");
gtk_widget_add_css_class(anim_subtitle, "subtitle-label");
gtk_box_append(GTK_BOX(anim_page), anim_subtitle);
GtkWidget *anim_scrolled = gtk_scrolled_window_new();
gtk_widget_set_vexpand(anim_scrolled, TRUE);
gtk_widget_set_hexpand(anim_scrolled, TRUE);
gtk_widget_set_margin_top(anim_scrolled, 10);
gtk_widget_set_margin_bottom(anim_scrolled, 15);
app_state.anim_text_view = gtk_text_view_new();
gtk_text_view_set_editable(GTK_TEXT_VIEW(app_state.anim_text_view), FALSE);
gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(app_state.anim_text_view), FALSE);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(app_state.anim_text_view), GTK_WRAP_WORD_CHAR);
gtk_widget_add_css_class(app_state.anim_text_view, "worry-text-view");
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(anim_scrolled), app_state.anim_text_view);
gtk_box_append(GTK_BOX(anim_page), anim_scrolled);
// Add dummy box to maintain layout height equivalent to input page
GtkWidget *dummy_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
GtkWidget *dummy_lbl = gtk_label_new("");
gtk_widget_set_size_request(dummy_lbl, -1, 48); // Match button container height approximately
gtk_box_append(GTK_BOX(dummy_box), dummy_lbl);
gtk_box_append(GTK_BOX(anim_page), dummy_box);
gtk_stack_add_named(GTK_STACK(app_state.stack), anim_page, "anim");
// ----------------------------------------------------
// PAGE 3: Consumed Page
// ----------------------------------------------------
GtkWidget *consumed_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20);
gtk_widget_set_valign(consumed_page, GTK_ALIGN_CENTER);
gtk_widget_set_halign(consumed_page, GTK_ALIGN_CENTER);
gtk_widget_set_vexpand(consumed_page, TRUE);
gtk_widget_set_hexpand(consumed_page, TRUE);
app_state.consumed_label = gtk_label_new("Consumed.");
gtk_widget_add_css_class(app_state.consumed_label, "consumed-label");
gtk_box_append(GTK_BOX(consumed_page), app_state.consumed_label);
GtkWidget *btn_reset = gtk_button_new_with_label("Return to the Void");
gtk_widget_add_css_class(btn_reset, "action-button");
gtk_widget_add_css_class(btn_reset, "btn-reset");
g_signal_connect(btn_reset, "clicked", G_CALLBACK(on_btn_reset_clicked), NULL);
gtk_box_append(GTK_BOX(consumed_page), btn_reset);
gtk_stack_add_named(GTK_STACK(app_state.stack), consumed_page, "consumed");
// Present window
gtk_window_present(GTK_WINDOW(window));
}
int run_gui(int argc, char* argv[], const char* initial_text) {
srand(time(NULL));
GtkApplication *app = gtk_application_new("org.antigravity.ScreamIntoTheVoid", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(activate), (gpointer)initial_text);
// We construct a clean arguments list for GTK to avoid standard GTK warnings
// about custom command line arguments.
int dummy_argc = 1;
char* dummy_argv[] = { argv[0], NULL };
int status = g_application_run(G_APPLICATION(app), dummy_argc, dummy_argv);
g_object_unref(app);
// Cleanup any lingering app state references
if (app_state.timer_id != 0) {
g_source_remove(app_state.timer_id);
}
if (app_state.original_text) {
g_free(app_state.original_text);
}
if (app_state.animated_text) {
g_free(app_state.animated_text);
}
return status;
}