Files
SuperNotepad/main.c

1271 lines
55 KiB
C

/*
* SuperNotepad (GTK3 + GtkSourceView4) - C
*
* Added:
* - Recent Files (GtkRecentManager + GtkRecentChooserMenu)
* - Dark mode detection (heuristic + notify on theme/prefer-dark changes)
* - Keyboard shortcuts (GtkAccelGroup)
*
* Build:
* meson setup build
* meson compile -C build
* ./build/SuperNotepad
*/
#include <gtk/gtk.h>
#include <gtksourceview/gtksource.h>
#include <glib/gstdio.h>
#include <time.h>
/* -------------------- App State -------------------- */
typedef struct {
GtkApplication *app;
GtkWidget *win;
GtkWidget *menubar;
GtkWidget *scroller;
GtkSourceView *view;
GtkSourceBuffer *buffer;
GtkWidget *status_bar_box;
GtkWidget *pos_label;
GtkWidget *state_label;
gboolean show_status_bar;
gboolean word_wrap;
gboolean show_line_numbers;
gboolean backups_enabled;
gchar *current_path; // NULL when untitled
gchar *backup_path; // derived
guint backup_timeout_id; // debounce timer id
// Recent files / shortcuts
GtkRecentManager *recent_manager;
GtkAccelGroup *accel_group;
// Dark mode detection
gboolean is_dark_mode;
// Find state
gchar *last_find;
GtkTextIter last_match_end;
gboolean has_last_match;
} AppState;
/* -------------------- Utility -------------------- */
static void free_and_null(gchar **p) {
if (*p) { g_free(*p); *p = NULL; }
}
static gchar* buffer_get_all_text(GtkTextBuffer *buf) {
GtkTextIter start, end;
gtk_text_buffer_get_start_iter(buf, &start);
gtk_text_buffer_get_end_iter(buf, &end);
return gtk_text_buffer_get_text(buf, &start, &end, TRUE);
}
static void update_title(AppState *s) {
const gchar *appname = "SuperNotepad";
gchar *title = NULL;
if (s->current_path && *s->current_path) {
gchar *base = g_path_get_basename(s->current_path);
title = g_strdup_printf("%s - %s", base, appname);
g_free(base);
} else {
title = g_strdup_printf("Untitled - %s", appname);
}
gtk_window_set_title(GTK_WINDOW(s->win), title);
g_free(title);
}
static void update_status(AppState *s) {
GtkTextIter iter;
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark);
int line = gtk_text_iter_get_line(&iter) + 1;
int col = gtk_text_iter_get_line_offset(&iter) + 1;
gchar *pos = g_strdup_printf("Ln %d, Col %d", line, col);
gtk_label_set_text(GTK_LABEL(s->pos_label), pos);
g_free(pos);
gboolean modified = gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer));
gtk_label_set_text(GTK_LABEL(s->state_label), modified ? "Modified (unsaved)" : "Saved");
}
/* -------------------- Preferences -------------------- */
static gchar* prefs_path(void) {
gchar *dir = g_build_filename(g_get_user_config_dir(), "SuperNotepad", NULL);
g_mkdir_with_parents(dir, 0700);
gchar *path = g_build_filename(dir, "config.ini", NULL);
g_free(dir);
return path;
}
static void prefs_save(AppState *s) {
GKeyFile *kf = g_key_file_new();
g_key_file_set_boolean(kf, "ui", "show_status_bar", s->show_status_bar);
g_key_file_set_boolean(kf, "ui", "word_wrap", s->word_wrap);
g_key_file_set_boolean(kf, "ui", "show_line_numbers", s->show_line_numbers);
g_key_file_set_boolean(kf, "edit", "backups_enabled", s->backups_enabled);
gsize len = 0;
gchar *data = g_key_file_to_data(kf, &len, NULL);
gchar *path = prefs_path();
(void)g_file_set_contents(path, data, (gssize)len, NULL);
g_free(path);
g_free(data);
g_key_file_unref(kf);
}
static void prefs_load(AppState *s) {
gchar *path = prefs_path();
GKeyFile *kf = g_key_file_new();
if (g_key_file_load_from_file(kf, path, G_KEY_FILE_NONE, NULL)) {
if (g_key_file_has_key(kf, "ui", "show_status_bar", NULL))
s->show_status_bar = g_key_file_get_boolean(kf, "ui", "show_status_bar", NULL);
if (g_key_file_has_key(kf, "ui", "word_wrap", NULL))
s->word_wrap = g_key_file_get_boolean(kf, "ui", "word_wrap", NULL);
if (g_key_file_has_key(kf, "ui", "show_line_numbers", NULL))
s->show_line_numbers = g_key_file_get_boolean(kf, "ui", "show_line_numbers", NULL);
if (g_key_file_has_key(kf, "edit", "backups_enabled", NULL))
s->backups_enabled = g_key_file_get_boolean(kf, "edit", "backups_enabled", NULL);
}
g_key_file_unref(kf);
g_free(path);
}
/* -------------------- View Settings -------------------- */
static void apply_view_settings(AppState *s) {
gtk_widget_set_visible(s->status_bar_box, s->show_status_bar);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(s->view),
s->word_wrap ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE);
gtk_source_view_set_show_line_numbers(s->view, s->show_line_numbers);
}
/* -------------------- Dark Mode Detection -------------------- */
static gboolean strcasestr_contains(const gchar *haystack, const gchar *needle) {
if (!haystack || !needle) return FALSE;
gchar *h = g_ascii_strdown(haystack, -1);
gchar *n = g_ascii_strdown(needle, -1);
gboolean ok = (strstr(h, n) != NULL);
g_free(h);
g_free(n);
return ok;
}
static void update_dark_mode(AppState *s) {
GtkSettings *settings = gtk_settings_get_default();
if (!settings) return;
gboolean prefer_dark = FALSE;
gchar *theme_name = NULL;
g_object_get(settings,
"gtk-application-prefer-dark-theme", &prefer_dark,
"gtk-theme-name", &theme_name,
NULL);
// Heuristic:
// - If prefer-dark is true OR theme name contains "dark", consider dark mode active.
gboolean is_dark = prefer_dark || (theme_name && strcasestr_contains(theme_name, "dark"));
if (s->is_dark_mode != is_dark) {
s->is_dark_mode = is_dark;
// For now we just track it (could later toggle CSS, icons, etc.)
g_message("Dark mode detected: %s (theme=%s prefer-dark=%s)",
s->is_dark_mode ? "ON" : "OFF",
theme_name ? theme_name : "(null)",
prefer_dark ? "true" : "false");
}
g_free(theme_name);
}
static void on_settings_notify(GtkSettings *settings, GParamSpec *pspec, gpointer user_data) {
(void)settings; (void)pspec;
AppState *s = (AppState*)user_data;
update_dark_mode(s);
}
/* -------------------- Backup Handling -------------------- */
static gchar* get_temp_untitled_backup_path(void) {
return g_build_filename(g_get_tmp_dir(), "SuperNotepad-Untitled.backup", NULL);
}
static gchar* compute_backup_path(AppState *s) {
if (s->current_path && *s->current_path) {
return g_strconcat(s->current_path, ".backup", NULL);
}
return get_temp_untitled_backup_path();
}
static void delete_backup_file_if_any(AppState *s) {
if (!s->backup_path) {
s->backup_path = compute_backup_path(s);
}
if (s->backup_path) {
(void)g_remove(s->backup_path);
}
}
static gboolean write_backup_now(gpointer user_data) {
AppState *s = (AppState*)user_data;
s->backup_timeout_id = 0;
if (!s->backups_enabled) return G_SOURCE_REMOVE;
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return G_SOURCE_REMOVE;
free_and_null(&s->backup_path);
s->backup_path = compute_backup_path(s);
gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer));
(void)g_file_set_contents(s->backup_path, text, -1, NULL);
g_free(text);
return G_SOURCE_REMOVE;
}
static void schedule_backup(AppState *s) {
if (!s->backups_enabled) return;
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return;
if (s->backup_timeout_id) {
g_source_remove(s->backup_timeout_id);
s->backup_timeout_id = 0;
}
s->backup_timeout_id = g_timeout_add(1000, write_backup_now, s);
}
/* -------------------- Recent Files Helpers -------------------- */
static void add_path_to_recent(AppState *s, const gchar *path) {
if (!s->recent_manager || !path || !*path) return;
GError *err = NULL;
gchar *uri = g_filename_to_uri(path, NULL, &err);
if (!uri) {
if (err) g_clear_error(&err);
return;
}
gtk_recent_manager_add_item(s->recent_manager, uri);
g_free(uri);
}
/* -------------------- File I/O -------------------- */
static gboolean save_to_path(AppState *s, const gchar *path) {
gchar *text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer));
GError *err = NULL;
gboolean ok = g_file_set_contents(path, text, -1, &err);
g_free(text);
if (!ok) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
"Save failed:\n%s", err ? err->message : "Unknown error");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
g_clear_error(&err);
return FALSE;
}
free_and_null(&s->current_path);
s->current_path = g_strdup(path);
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
update_title(s);
update_status(s);
delete_backup_file_if_any(s);
add_path_to_recent(s, path);
return TRUE;
}
static gboolean open_from_path(AppState *s, const gchar *path) {
if (!path || !*path) return FALSE;
gchar *contents = NULL;
gsize len = 0;
GError *err = NULL;
if (!g_file_get_contents(path, &contents, &len, &err)) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
"Open failed:\n%s", err ? err->message : "Unknown error");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
g_clear_error(&err);
return FALSE;
}
gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), contents, (gint)len);
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
free_and_null(&s->current_path);
s->current_path = g_strdup(path);
free_and_null(&s->backup_path);
free_and_null(&s->last_find);
s->has_last_match = FALSE;
update_title(s);
update_status(s);
g_free(contents);
add_path_to_recent(s, path);
return TRUE;
}
static gboolean do_save_as(AppState *s) {
GtkWidget *dlg = gtk_file_chooser_dialog_new("Save As",
GTK_WINDOW(s->win),
GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Save", GTK_RESPONSE_ACCEPT,
NULL);
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE);
if (s->current_path) {
gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dlg), s->current_path);
}
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return FALSE; }
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
gtk_widget_destroy(dlg);
if (!filename) return FALSE;
gboolean ok = save_to_path(s, filename);
g_free(filename);
return ok;
}
/* Clean close/save prompt */
static gboolean ensure_saved_or_cancel(AppState *s) {
if (!gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return TRUE;
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_NONE,
"Save changes to this document?");
gtk_dialog_add_buttons(GTK_DIALOG(d),
"Cancel", GTK_RESPONSE_CANCEL,
"Don't Save", GTK_RESPONSE_REJECT,
"Save", GTK_RESPONSE_ACCEPT,
NULL);
int resp = gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
if (resp == GTK_RESPONSE_CANCEL) return FALSE;
if (resp == GTK_RESPONSE_REJECT) return TRUE;
if (s->current_path) {
return save_to_path(s, s->current_path);
}
return do_save_as(s);
}
static gboolean on_window_delete_event(GtkWidget *w, GdkEvent *e, gpointer user_data) {
(void)w; (void)e;
AppState *s = (AppState*)user_data;
if (!ensure_saved_or_cancel(s)) return TRUE; // block close
return FALSE; // allow close
}
/* -------------------- Actions: File Menu -------------------- */
static void action_new(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
if (!ensure_saved_or_cancel(s)) return;
gtk_text_buffer_set_text(GTK_TEXT_BUFFER(s->buffer), "", -1);
gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(s->buffer), FALSE);
free_and_null(&s->current_path);
free_and_null(&s->backup_path);
free_and_null(&s->last_find);
s->has_last_match = FALSE;
update_title(s);
update_status(s);
delete_backup_file_if_any(s);
}
static void action_open(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
if (!ensure_saved_or_cancel(s)) return;
GtkWidget *dlg = gtk_file_chooser_dialog_new("Open",
GTK_WINDOW(s->win),
GTK_FILE_CHOOSER_ACTION_OPEN,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT,
NULL);
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg));
gtk_widget_destroy(dlg);
if (!filename) return;
(void)open_from_path(s, filename);
g_free(filename);
}
static void action_save(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
if (!s->current_path) {
(void)do_save_as(s);
return;
}
(void)save_to_path(s, s->current_path);
}
static void action_save_as(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
(void)do_save_as(s);
}
static void action_exit(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
gtk_window_close(GTK_WINDOW(s->win));
}
/* Recent chooser callback */
static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer user_data) {
AppState *s = (AppState*)user_data;
if (!ensure_saved_or_cancel(s)) return;
gchar *uri = gtk_recent_chooser_get_current_uri(chooser);
if (!uri) return;
GError *err = NULL;
gchar *filename = g_filename_from_uri(uri, NULL, &err);
g_free(uri);
if (!filename) {
if (err) g_clear_error(&err);
return;
}
(void)open_from_path(s, filename);
g_free(filename);
}
/* -------------------- Print: Real pagination -------------------- */
typedef struct {
GtkSourcePrintCompositor *comp;
} PrintState;
static void on_print_begin(GtkPrintOperation *op, GtkPrintContext *ctx, gpointer user_data) {
PrintState *ps = (PrintState*)user_data;
while (!gtk_source_print_compositor_paginate(ps->comp, ctx)) { }
int pages = gtk_source_print_compositor_get_n_pages(ps->comp);
gtk_print_operation_set_n_pages(op, pages);
}
static void on_print_draw_page(GtkPrintOperation *op, GtkPrintContext *ctx, int page_nr, gpointer user_data) {
(void)op;
PrintState *ps = (PrintState*)user_data;
gtk_source_print_compositor_draw_page(ps->comp, ctx, page_nr);
}
static void on_print_done(GtkPrintOperation *op, GtkPrintOperationResult res, gpointer user_data) {
(void)op; (void)res;
PrintState *ps = (PrintState*)user_data;
if (ps->comp) g_object_unref(ps->comp);
g_free(ps);
}
static void action_print(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
PrintState *ps = g_new0(PrintState, 1);
ps->comp = gtk_source_print_compositor_new(GTK_SOURCE_BUFFER(s->buffer));
GtkPrintOperation *op = gtk_print_operation_new();
gtk_print_operation_set_job_name(op, "SuperNotepad Print");
g_signal_connect(op, "begin-print", G_CALLBACK(on_print_begin), ps);
g_signal_connect(op, "draw-page", G_CALLBACK(on_print_draw_page), ps);
g_signal_connect(op, "done", G_CALLBACK(on_print_done), ps);
GError *err = NULL;
GtkPrintOperationResult r = gtk_print_operation_run(op,
GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
GTK_WINDOW(s->win),
&err);
if (r == GTK_PRINT_OPERATION_RESULT_ERROR) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
"Print failed:\n%s", err ? err->message : "Unknown error");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
g_clear_error(&err);
}
g_object_unref(op);
}
/* -------------------- Actions: Edit Menu -------------------- */
static void action_undo(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
if (gtk_source_buffer_can_undo(s->buffer)) gtk_source_buffer_undo(s->buffer);
}
static void action_cut(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
gtk_text_buffer_cut_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, TRUE);
}
static void action_copy(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
gtk_text_buffer_copy_clipboard(GTK_TEXT_BUFFER(s->buffer), cb);
}
static void action_paste(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkClipboard *cb = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
gtk_text_buffer_paste_clipboard(GTK_TEXT_BUFFER(s->buffer), cb, NULL, TRUE);
}
static void action_delete(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkTextIter start, end;
if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &start, &end)) {
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &start, &end);
}
}
static void action_select_all(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkTextIter start, end;
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &start);
gtk_text_buffer_get_end_iter(GTK_TEXT_BUFFER(s->buffer), &end);
gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &start, &end);
}
static void action_time_date(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
time_t t = time(NULL);
struct tm lt;
#if defined(_WIN32)
localtime_s(&lt, &t);
#else
localtime_r(&t, &lt);
#endif
char buf[128];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", &lt);
GtkTextIter iter;
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &iter, mark);
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &iter, buf, -1);
}
/* -------------------- Find / Replace / Go To -------------------- */
static gboolean do_find_from_iter(AppState *s, const gchar *needle, GtkTextIter *from, gboolean wrap) {
if (!needle || !*needle) return FALSE;
GtkTextIter start = *from;
GtkTextIter match_start, match_end;
gboolean found = gtk_text_iter_forward_search(&start, needle,
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
&match_start, &match_end, NULL);
if (!found && wrap) {
GtkTextIter begin;
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &begin);
found = gtk_text_iter_forward_search(&begin, needle,
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
&match_start, &match_end, NULL);
}
if (found) {
gtk_text_buffer_select_range(GTK_TEXT_BUFFER(s->buffer), &match_start, &match_end);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &match_start, 0.2, FALSE, 0, 0);
s->last_match_end = match_end;
s->has_last_match = TRUE;
return TRUE;
}
return FALSE;
}
static void action_find(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkWidget *dlg = gtk_dialog_new_with_buttons("Find",
GTK_WINDOW(s->win),
GTK_DIALOG_MODAL,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Find", GTK_RESPONSE_ACCEPT,
NULL);
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6);
gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 6);
if (s->last_find) gtk_entry_set_text(GTK_ENTRY(entry), s->last_find);
gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT);
gtk_widget_show_all(dlg);
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
const gchar *needle = gtk_entry_get_text(GTK_ENTRY(entry));
free_and_null(&s->last_find);
s->last_find = g_strdup(needle);
GtkTextIter from;
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &from, mark);
gboolean ok = do_find_from_iter(s, needle, &from, TRUE);
gtk_widget_destroy(dlg);
if (!ok) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
"Text not found.");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
}
}
static void action_find_next(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
if (!s->last_find || !*s->last_find) {
action_find(NULL, s);
return;
}
GtkTextIter from;
if (s->has_last_match) {
from = s->last_match_end;
} else {
GtkTextMark *mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(s->buffer));
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(s->buffer), &from, mark);
}
gboolean ok = do_find_from_iter(s, s->last_find, &from, TRUE);
if (!ok) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
"No further matches.");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
}
}
static void action_go_to(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkWidget *dlg = gtk_dialog_new_with_buttons("Go To Line",
GTK_WINDOW(s->win),
GTK_DIALOG_MODAL,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Go", GTK_RESPONSE_ACCEPT,
NULL);
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
GtkWidget *spin = gtk_spin_button_new_with_range(1, 1000000, 1);
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Line number:"), FALSE, FALSE, 6);
gtk_box_pack_start(GTK_BOX(box), spin, FALSE, FALSE, 6);
gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_ACCEPT);
gtk_widget_show_all(dlg);
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
if (resp != GTK_RESPONSE_ACCEPT) { gtk_widget_destroy(dlg); return; }
int line = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(spin));
gtk_widget_destroy(dlg);
GtkTextIter iter;
gtk_text_buffer_get_iter_at_line(GTK_TEXT_BUFFER(s->buffer), &iter, line - 1);
gtk_text_buffer_place_cursor(GTK_TEXT_BUFFER(s->buffer), &iter);
gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(s->view), &iter, 0.2, FALSE, 0, 0);
update_status(s);
}
static void action_replace(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkWidget *dlg = gtk_dialog_new_with_buttons("Replace",
GTK_WINDOW(s->win),
GTK_DIALOG_MODAL,
"_Close", GTK_RESPONSE_CLOSE,
"_Replace", 1,
"Replace _All", 2,
NULL);
GtkWidget *box = gtk_dialog_get_content_area(GTK_DIALOG(dlg));
GtkWidget *find_entry = gtk_entry_new();
GtkWidget *rep_entry = gtk_entry_new();
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Find what:"), FALSE, FALSE, 6);
gtk_box_pack_start(GTK_BOX(box), find_entry, FALSE, FALSE, 6);
gtk_box_pack_start(GTK_BOX(box), gtk_label_new("Replace with:"), FALSE, FALSE, 6);
gtk_box_pack_start(GTK_BOX(box), rep_entry, FALSE, FALSE, 6);
if (s->last_find) gtk_entry_set_text(GTK_ENTRY(find_entry), s->last_find);
gtk_widget_show_all(dlg);
for (;;) {
int resp = gtk_dialog_run(GTK_DIALOG(dlg));
if (resp == GTK_RESPONSE_CLOSE) break;
const gchar *needle = gtk_entry_get_text(GTK_ENTRY(find_entry));
const gchar *repl = gtk_entry_get_text(GTK_ENTRY(rep_entry));
if (!needle || !*needle) {
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
"Enter text to find.");
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
continue;
}
free_and_null(&s->last_find);
s->last_find = g_strdup(needle);
if (resp == 1) {
GtkTextIter sel_start, sel_end;
if (gtk_text_buffer_get_selection_bounds(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end)) {
gchar *selected = gtk_text_buffer_get_text(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end, TRUE);
gboolean matches = (g_strcmp0(selected, needle) == 0);
g_free(selected);
if (matches) {
gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer));
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &sel_start, &sel_end);
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &sel_start, repl, -1);
gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer));
GtkTextIter from = sel_start;
(void)do_find_from_iter(s, needle, &from, TRUE);
continue;
}
}
action_find_next(NULL, s);
continue;
}
if (resp == 2) {
GtkTextIter iter;
gtk_text_buffer_get_start_iter(GTK_TEXT_BUFFER(s->buffer), &iter);
int count = 0;
gtk_text_buffer_begin_user_action(GTK_TEXT_BUFFER(s->buffer));
while (1) {
GtkTextIter ms, me;
gboolean found = gtk_text_iter_forward_search(&iter, needle,
GTK_TEXT_SEARCH_TEXT_ONLY | GTK_TEXT_SEARCH_VISIBLE_ONLY,
&ms, &me, NULL);
if (!found) break;
gtk_text_buffer_delete(GTK_TEXT_BUFFER(s->buffer), &ms, &me);
gtk_text_buffer_insert(GTK_TEXT_BUFFER(s->buffer), &ms, repl, -1);
iter = ms;
count++;
}
gtk_text_buffer_end_user_action(GTK_TEXT_BUFFER(s->buffer));
GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
"Replaced %d occurrence(s).", count);
gtk_dialog_run(GTK_DIALOG(d));
gtk_widget_destroy(d);
continue;
}
}
gtk_widget_destroy(dlg);
}
/* -------------------- Toggles (View/Edit) -------------------- */
static void toggle_status_bar(GtkCheckMenuItem *mi, gpointer user_data) {
AppState *s = (AppState*)user_data;
s->show_status_bar = gtk_check_menu_item_get_active(mi);
apply_view_settings(s);
prefs_save(s);
}
static void toggle_word_wrap(GtkCheckMenuItem *mi, gpointer user_data) {
AppState *s = (AppState*)user_data;
s->word_wrap = gtk_check_menu_item_get_active(mi);
apply_view_settings(s);
prefs_save(s);
}
static void toggle_line_numbers(GtkCheckMenuItem *mi, gpointer user_data) {
AppState *s = (AppState*)user_data;
s->show_line_numbers = gtk_check_menu_item_get_active(mi);
apply_view_settings(s);
prefs_save(s);
}
static void toggle_backups(GtkCheckMenuItem *mi, gpointer user_data) {
AppState *s = (AppState*)user_data;
s->backups_enabled = gtk_check_menu_item_get_active(mi);
if (!s->backups_enabled) {
if (s->backup_timeout_id) {
g_source_remove(s->backup_timeout_id);
s->backup_timeout_id = 0;
}
delete_backup_file_if_any(s);
} else {
schedule_backup(s);
}
prefs_save(s);
}
/* -------------------- About -------------------- */
static void action_about(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
GtkWidget *dlg = gtk_about_dialog_new();
gtk_window_set_transient_for(GTK_WINDOW(dlg), GTK_WINDOW(s->win));
gtk_window_set_modal(GTK_WINDOW(dlg), TRUE);
gtk_about_dialog_set_program_name(GTK_ABOUT_DIALOG(dlg), "SuperNotepad");
gtk_about_dialog_set_version(GTK_ABOUT_DIALOG(dlg), "0.2");
const gchar *authors[] = { "John Paul Wohlscheid", "ChatGPT", NULL };
gtk_about_dialog_set_authors(GTK_ABOUT_DIALOG(dlg), authors);
gtk_about_dialog_set_comments(GTK_ABOUT_DIALOG(dlg),
"SuperNotepad 0.2\nLicensed under The Unlicense.");
gtk_about_dialog_set_license(GTK_ABOUT_DIALOG(dlg),
"This is free and unencumbered software released into the public domain.\n"
"For more information, see The Unlicense.");
gtk_about_dialog_set_website(GTK_ABOUT_DIALOG(dlg), "https://unlicense.org");
gtk_about_dialog_set_website_label(GTK_ABOUT_DIALOG(dlg), "The Unlicense (https://unlicense.org)");
gtk_dialog_run(GTK_DIALOG(dlg));
gtk_widget_destroy(dlg);
}
static void action_keyboard_shortcuts(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
const char *msg =
"Keyboard Shortcuts\n"
"\n"
"File\n"
" Ctrl+N New\n"
" Ctrl+O Open\n"
" Ctrl+S Save\n"
" Ctrl+Shift+S Save As\n"
" Ctrl+P Print\n"
" Ctrl+Q Exit\n"
"\n"
"Edit\n"
" Ctrl+Z Undo\n"
" Ctrl+X Cut\n"
" Ctrl+C Copy\n"
" Ctrl+V Paste\n"
" Delete Delete selection\n"
" Ctrl+F Find\n"
" F3 Find Next\n"
" Ctrl+H Replace\n"
" Ctrl+G Go To\n"
" Ctrl+A Select All\n"
" F5 Time/Date\n"
"\n"
"View\n"
" Ctrl+Shift+W Toggle Word Wrap\n"
" Ctrl+Shift+L Toggle Line Numbering\n"
" Ctrl+Alt+B Toggle Status Bar\n"
"\n"
"Help\n"
" F1 About\n";
GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(s->win),
GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_OK,
"%s", msg);
gtk_window_set_title(GTK_WINDOW(dlg), "Keyboard Shortcuts");
gtk_dialog_run(GTK_DIALOG(dlg));
gtk_widget_destroy(dlg);
}
/* -------------------- Buffer signals -------------------- */
static void on_mark_set(GtkTextBuffer *buf, GtkTextIter *new_loc, GtkTextMark *mark, gpointer user_data) {
(void)buf; (void)new_loc; (void)mark;
AppState *s = (AppState*)user_data;
update_status(s);
}
static void on_modified_changed(GtkTextBuffer *buf, gpointer user_data) {
(void)buf;
AppState *s = (AppState*)user_data;
update_status(s);
}
static void on_buffer_changed(GtkTextBuffer *buf, gpointer user_data) {
(void)buf;
AppState *s = (AppState*)user_data;
update_status(s);
schedule_backup(s);
}
/* -------------------- Menu Helpers + Shortcuts -------------------- */
static GtkWidget* menu_item(AppState *s, const gchar *label, GCallback cb) {
GtkWidget *mi = gtk_menu_item_new_with_label(label);
if (cb) g_signal_connect(mi, "activate", cb, s);
return mi;
}
static GtkWidget* menu_item_accel(AppState *s, const gchar *label, GCallback cb,
guint keyval, GdkModifierType mods) {
GtkWidget *mi = menu_item(s, label, cb);
if (s->accel_group && cb && keyval != 0) {
gtk_widget_add_accelerator(mi, "activate", s->accel_group,
keyval, mods, GTK_ACCEL_VISIBLE);
}
return mi;
}
static GtkWidget* check_menu_item(AppState *s, const gchar *label, gboolean active,
GCallback toggled_cb,
guint keyval, GdkModifierType mods) {
GtkWidget *mi = gtk_check_menu_item_new_with_label(label);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), active);
if (toggled_cb) g_signal_connect(mi, "toggled", toggled_cb, s);
// For check items, accelerator triggers "activate" which toggles it.
if (s->accel_group && keyval != 0) {
gtk_widget_add_accelerator(mi, "activate", s->accel_group,
keyval, mods, GTK_ACCEL_VISIBLE);
}
return mi;
}
/* Builds the menubar (includes Open Recent) */
static GtkWidget* build_menubar(AppState *s) {
GtkWidget *menubar = gtk_menu_bar_new();
/* File */
GtkWidget *file_menu = gtk_menu_new();
GtkWidget *file_root = gtk_menu_item_new_with_label("File");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_root), file_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "New", G_CALLBACK(action_new), GDK_KEY_n, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "Open", G_CALLBACK(action_open), GDK_KEY_o, GDK_CONTROL_MASK));
// Open Recent (GtkRecentChooserMenu)
GtkWidget *recent_menu = gtk_recent_chooser_menu_new_for_manager(s->recent_manager);
gtk_recent_chooser_set_limit(GTK_RECENT_CHOOSER(recent_menu), 15);
gtk_recent_chooser_set_show_not_found(GTK_RECENT_CHOOSER(recent_menu), FALSE);
gtk_recent_chooser_set_show_private(GTK_RECENT_CHOOSER(recent_menu), FALSE);
gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(recent_menu), TRUE);
g_signal_connect(recent_menu, "item-activated", G_CALLBACK(on_recent_item_activated), s);
GtkWidget *open_recent = gtk_menu_item_new_with_label("Open Recent");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(open_recent), recent_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_recent);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "Save", G_CALLBACK(action_save), GDK_KEY_s, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "Save As", G_CALLBACK(action_save_as), GDK_KEY_s, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK)));
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "Print", G_CALLBACK(action_print), GDK_KEY_p, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu),
menu_item_accel(s, "Exit", G_CALLBACK(action_exit), GDK_KEY_q, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), file_root);
/* Edit */
GtkWidget *edit_menu = gtk_menu_new();
GtkWidget *edit_root = gtk_menu_item_new_with_label("Edit");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(edit_root), edit_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Undo", G_CALLBACK(action_undo), GDK_KEY_z, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Cut", G_CALLBACK(action_cut), GDK_KEY_x, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Copy", G_CALLBACK(action_copy), GDK_KEY_c, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Paste", G_CALLBACK(action_paste), GDK_KEY_v, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Delete", G_CALLBACK(action_delete), GDK_KEY_Delete, (GdkModifierType)0));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Find", G_CALLBACK(action_find), GDK_KEY_f, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Find Next", G_CALLBACK(action_find_next), GDK_KEY_F3, (GdkModifierType)0));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Replace", G_CALLBACK(action_replace), GDK_KEY_h, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Go To", G_CALLBACK(action_go_to), GDK_KEY_g, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Select All", G_CALLBACK(action_select_all), GDK_KEY_a, GDK_CONTROL_MASK));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
menu_item_accel(s, "Time/Date", G_CALLBACK(action_time_date), GDK_KEY_F5, (GdkModifierType)0));
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu),
check_menu_item(s, "Create .backup when unsaved", s->backups_enabled, G_CALLBACK(toggle_backups),
GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), edit_root);
/* View */
GtkWidget *view_menu = gtk_menu_new();
GtkWidget *view_root = gtk_menu_item_new_with_label("View");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_root), view_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
check_menu_item(s, "Status Bar", s->show_status_bar, G_CALLBACK(toggle_status_bar),
GDK_KEY_b, (GdkModifierType)(GDK_CONTROL_MASK | GDK_MOD1_MASK)));
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
check_menu_item(s, "Word Wrap", s->word_wrap, G_CALLBACK(toggle_word_wrap),
GDK_KEY_w, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu),
check_menu_item(s, "Line Numbering", s->show_line_numbers, G_CALLBACK(toggle_line_numbers),
GDK_KEY_l, (GdkModifierType)(GDK_CONTROL_MASK | GDK_SHIFT_MASK)));
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), view_root);
/* Help */
GtkWidget *help_menu = gtk_menu_new();
GtkWidget *help_root = gtk_menu_item_new_with_label("Help");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_root), help_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu),
menu_item(s, "Keyboard Shortcuts", G_CALLBACK(action_keyboard_shortcuts)));
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu),
menu_item_accel(s, "About", G_CALLBACK(action_about), GDK_KEY_F1, (GdkModifierType)0));
gtk_menu_shell_append(GTK_MENU_SHELL(menubar), help_root);
return menubar;
}
/* -------------------- Cleanup -------------------- */
static void on_window_destroy(GtkWidget *w, gpointer user_data) {
(void)w;
AppState *s = (AppState*)user_data;
prefs_save(s);
if (s->backup_timeout_id) {
g_source_remove(s->backup_timeout_id);
s->backup_timeout_id = 0;
}
free_and_null(&s->current_path);
free_and_null(&s->backup_path);
free_and_null(&s->last_find);
g_free(s);
}
/* -------------------- App Init -------------------- */
static void app_activate(GtkApplication *app, gpointer user_data) {
(void)user_data;
AppState *s = g_new0(AppState, 1);
s->app = app;
// Defaults
s->show_status_bar = TRUE;
s->word_wrap = FALSE;
s->show_line_numbers = TRUE;
s->backups_enabled = TRUE;
// Recent manager + accel group
s->recent_manager = gtk_recent_manager_get_default();
s->accel_group = gtk_accel_group_new();
// Load saved prefs (overrides defaults if present)
prefs_load(s);
s->win = gtk_application_window_new(app);
gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600);
// Attach accelerators to window
gtk_window_add_accel_group(GTK_WINDOW(s->win), s->accel_group);
// Close prompt
g_signal_connect(s->win, "delete-event", G_CALLBACK(on_window_delete_event), s);
// Dark mode detection
GtkSettings *settings = gtk_settings_get_default();
if (settings) {
g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(on_settings_notify), s);
g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(on_settings_notify), s);
}
// Main layout
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(s->win), root);
// Menubar
s->menubar = build_menubar(s);
gtk_box_pack_start(GTK_BOX(root), s->menubar, FALSE, FALSE, 0);
// Editor
s->buffer = GTK_SOURCE_BUFFER(gtk_source_buffer_new(NULL));
s->view = GTK_SOURCE_VIEW(gtk_source_view_new_with_buffer(s->buffer));
s->scroller = gtk_scrolled_window_new(NULL, NULL);
gtk_container_add(GTK_CONTAINER(s->scroller), GTK_WIDGET(s->view));
gtk_box_pack_start(GTK_BOX(root), s->scroller, TRUE, TRUE, 0);
// Status bar area
s->status_bar_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_start(s->status_bar_box, 8);
gtk_widget_set_margin_end(s->status_bar_box, 8);
gtk_widget_set_margin_top(s->status_bar_box, 4);
gtk_widget_set_margin_bottom(s->status_bar_box, 4);
s->pos_label = gtk_label_new("Ln 1, Col 1");
s->state_label = gtk_label_new("Saved");
gtk_box_pack_start(GTK_BOX(s->status_bar_box), s->pos_label, FALSE, FALSE, 0);
gtk_box_pack_end(GTK_BOX(s->status_bar_box), s->state_label, FALSE, FALSE, 0);
gtk_box_pack_end(GTK_BOX(root), s->status_bar_box, FALSE, FALSE, 0);
apply_view_settings(s);
update_title(s);
update_status(s);
// Buffer signals
g_signal_connect(s->buffer, "mark-set", G_CALLBACK(on_mark_set), s);
g_signal_connect(s->buffer, "modified-changed", G_CALLBACK(on_modified_changed), s);
g_signal_connect(s->buffer, "changed", G_CALLBACK(on_buffer_changed), s);
// Cleanup
g_signal_connect(s->win, "destroy", G_CALLBACK(on_window_destroy), s);
gtk_widget_show_all(s->win);
// Run initial dark mode detection once window exists
update_dark_mode(s);
}
int main(int argc, char **argv) {
GtkApplication *app = gtk_application_new("com.example.SuperNotepad", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(app_activate), NULL);
int status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}