commit b6406a205a0f064e981d8d1bdd5e7647b44134e3 Author: John Paul Wohlscheid Date: Mon Feb 16 14:53:28 2026 -0500 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e04aa0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +.mypy_cache/ +.pytest_cache/ +/.project +/.pydevproject +/.settings +/.cproject +/.idea +/.vscode + +__pycache__ +/.coverage/ +/.coveragerc +/install dir +/work area + +/meson-test-run.txt +/meson-test-run.xml +/meson-cross-test-run.txt +/meson-cross-test-run.xml + +.DS_Store +*~ +*.swp +packagecache +.wraplock +/MANIFEST +/build +/dist +/meson.egg-info + +/docs/built_docs +/docs/hotdoc-private* + +*.pyc +/*venv* diff --git a/main.c b/main.c new file mode 100644 index 0000000..937797d --- /dev/null +++ b/main.c @@ -0,0 +1,1085 @@ +/* + SuperNotepad (GTK3 + GtkSourceView) - C + Features: + - File/Edit/View/Help menus + - Status bar: Ln/Col + Saved/Modified + - Word wrap / Status bar / Line numbers toggles + - .backup file auto-write when modified & unsaved (toggleable) + - Find, Find Next, Replace, Go To + - Time/Date insert + - Print (simple) + + Build: + meson setup build + meson compile -C build + ./build/SuperNotepad +*/ + +#include +#include +#include +#include + +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 or temp + guint backup_timeout_id; // debounce + + // 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* get_temp_untitled_backup_path(void) { + // Keep it predictable and easy to find + 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 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 *name = "SuperNotepad"; + gchar *title = NULL; + + if (s->current_path) { + gchar *base = g_path_get_basename(s->current_path); + title = g_strdup_printf("%s - %s", base, name); + g_free(base); + } else { + title = g_strdup_printf("Untitled - %s", name); + } + + 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"); +} + +/* ---------- Backup handling ---------- */ + +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)); + GError *err = NULL; + + if (!g_file_set_contents(s->backup_path, text, -1, &err)) { + g_warning("Failed writing backup '%s': %s", s->backup_path, err->message); + g_clear_error(&err); + } + + 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; + + // Debounce: write 1 second after last change + if (s->backup_timeout_id != 0) return; + s->backup_timeout_id = g_timeout_add(1000, write_backup_now, s); +} + +static void delete_backup_file_if_any(AppState *s) { + if (!s->backup_path) { + // compute path once, because we may never have written yet + s->backup_path = compute_backup_path(s); + } + + if (s->backup_path) { + GError *err = NULL; + if (g_remove(s->backup_path) != 0) { + // ignore failures (file may not exist) + } + if (err) g_clear_error(&err); + } +} + +/* ---------- File I/O ---------- */ + +static gboolean maybe_confirm_discard(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_WARNING, + GTK_BUTTONS_NONE, + "This document has unsaved changes."); + gtk_dialog_add_buttons(GTK_DIALOG(d), + "Cancel", GTK_RESPONSE_CANCEL, + "Discard", GTK_RESPONSE_REJECT, + "Save", GTK_RESPONSE_ACCEPT, + NULL); + + int resp = gtk_dialog_run(GTK_DIALOG(d)); + gtk_widget_destroy(d); + + if (resp == GTK_RESPONSE_ACCEPT) { + // Save + // If no path, force Save As + return FALSE; // caller will handle save flow by calling save action + } + if (resp == GTK_RESPONSE_REJECT) return TRUE; + return FALSE; +} + +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); + + // Once saved, remove backup (it’s no longer “unsaved”) + delete_backup_file_if_any(s); + + return TRUE; +} + +static void action_new(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { + GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + "Save changes before creating a new 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; + if (resp == GTK_RESPONSE_ACCEPT) { + // delegate to save; if no path, do Save As + // We'll just call the save handler and let it decide. + // If user cancels save dialog, abort new. + // The easiest is: do a Save As if no path, else Save. + // We'll implement inline here. + + if (s->current_path) { + if (!save_to_path(s, s->current_path)) return; + } else { + 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); + int r = gtk_dialog_run(GTK_DIALOG(dlg)); + if (r == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); + gtk_widget_destroy(dlg); + if (!filename) return; + gboolean ok = save_to_path(s, filename); + g_free(filename); + if (!ok) return; + } else { + gtk_widget_destroy(dlg); + return; + } + } + } + // else "Don't Save": continue + } + + 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->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 (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { + GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + "Save changes before opening another file?"); + 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; + if (resp == GTK_RESPONSE_ACCEPT) { + if (s->current_path) { + if (!save_to_path(s, s->current_path)) return; + } else { + GtkWidget *sd = 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(sd), TRUE); + int r = gtk_dialog_run(GTK_DIALOG(sd)); + if (r == GTK_RESPONSE_ACCEPT) { + char *fn = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(sd)); + gtk_widget_destroy(sd); + if (!fn) return; + gboolean ok = save_to_path(s, fn); + g_free(fn); + if (!ok) return; + } else { + gtk_widget_destroy(sd); + 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; + + gchar *contents = NULL; + gsize len = 0; + GError *err = NULL; + + if (!g_file_get_contents(filename, &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); + g_free(filename); + return; + } + + 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(filename); + + free_and_null(&s->last_find); + s->has_last_match = FALSE; + + update_title(s); + update_status(s); + + // backup path changes with opened file + free_and_null(&s->backup_path); + + g_free(contents); + g_free(filename); +} + +static void action_save_as(GtkWidget *w, gpointer user_data); + +static void action_save(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + if (!s->current_path) { + action_save_as(NULL, s); + return; + } + + save_to_path(s, s->current_path); +} + +static void action_save_as(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + 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; } + + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); + gtk_widget_destroy(dlg); + if (!filename) return; + + save_to_path(s, filename); + + g_free(filename); +} + +static void action_exit(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) { + GtkWidget *d = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + "Save changes before exiting?"); + 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; + if (resp == GTK_RESPONSE_ACCEPT) { + if (s->current_path) { + if (!save_to_path(s, s->current_path)) return; + } else { + action_save_as(NULL, s); + if (gtk_text_buffer_get_modified(GTK_TEXT_BUFFER(s->buffer))) return; // user canceled + } + } + } + + gtk_window_close(GTK_WINDOW(s->win)); +} + +/* ---------- Print ---------- */ + +typedef struct { + gchar *text; +} PrintData; + +static void print_data_free(PrintData *pd) { + if (!pd) return; + g_free(pd->text); + g_free(pd); +} + +static void on_print_begin(GtkPrintOperation *op, GtkPrintContext *ctx, gpointer user_data) { + (void)op; (void)ctx; (void)user_data; + // single page simplistic; could paginate by measuring +} + +static void on_print_draw_page(GtkPrintOperation *op, GtkPrintContext *ctx, int page_nr, gpointer user_data) { + (void)op; (void)page_nr; + PrintData *pd = (PrintData*)user_data; + + cairo_t *cr = gtk_print_context_get_cairo_context(ctx); + PangoLayout *layout = gtk_print_context_create_pango_layout(ctx); + + pango_layout_set_text(layout, pd->text ? pd->text : "", -1); + + // Wrap to page width + double width = gtk_print_context_get_width(ctx); + pango_layout_set_width(layout, (int)(width * PANGO_SCALE)); + pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); + + cairo_move_to(cr, 0, 0); + pango_cairo_show_layout(cr, layout); + + g_object_unref(layout); +} + +static void action_print(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + PrintData *pd = g_new0(PrintData, 1); + pd->text = buffer_get_all_text(GTK_TEXT_BUFFER(s->buffer)); + + GtkPrintOperation *op = gtk_print_operation_new(); + gtk_print_operation_set_job_name(op, "SuperNotepad Print"); + gtk_print_operation_set_n_pages(op, 1); + + g_signal_connect(op, "begin-print", G_CALLBACK(on_print_begin), pd); + g_signal_connect(op, "draw-page", G_CALLBACK(on_print_draw_page), pd); + + GError *err = NULL; + GtkPrintOperationResult res = gtk_print_operation_run(op, + GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, + GTK_WINDOW(s->win), + &err); + + if (res == 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); + print_data_free(pd); +} + +/* ---------- Edit actions ---------- */ + +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(<, &t); +#else + localtime_r(&t, <); +#endif + char buf[128]; + strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", <); + + 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) { + // Replace: if selection matches needle, replace it; else find next + 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)); + // After replace, find next occurrence + GtkTextIter from = sel_start; + do_find_from_iter(s, needle, &from, TRUE); + continue; + } + } + + action_find_next(NULL, s); + continue; + } + + if (resp == 2) { + // Replace All + 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; // continue after inserted text + 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); +} + +/* ---------- View toggles ---------- */ + +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); +} + +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); +} + +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); +} + +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); +} + +/* ---------- Backup toggle (Edit menu) ---------- */ + +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) { + // Stop pending backup and remove any backup file + if (s->backup_timeout_id) { + g_source_remove(s->backup_timeout_id); + s->backup_timeout_id = 0; + } + delete_backup_file_if_any(s); + } else { + // If currently modified, schedule one + schedule_backup(s); + } +} + +/* ---------- Help / About ---------- */ + +static void action_about(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(s->win), + GTK_DIALOG_MODAL, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "SuperNotepage 0.1 by John Paul Wohlscheid and ChatGPT"); + gtk_window_set_title(GTK_WINDOW(dlg), "About"); + 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 building helpers ---------- */ + +static GtkWidget* menu_item(const gchar *label, GCallback cb, AppState *s) { + GtkWidget *mi = gtk_menu_item_new_with_label(label); + if (cb) g_signal_connect(mi, "activate", cb, s); + return mi; +} + +static GtkWidget* check_menu_item(const gchar *label, gboolean active, GCallback toggled_cb, AppState *s) { + 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); + return mi; +} + +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("New", G_CALLBACK(action_new), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Open", G_CALLBACK(action_open), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Save", G_CALLBACK(action_save), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Save As", G_CALLBACK(action_save_as), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), menu_item("Print", G_CALLBACK(action_print), s)); + 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("Exit", G_CALLBACK(action_exit), s)); + + 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("Undo", G_CALLBACK(action_undo), s)); + 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("Cut", G_CALLBACK(action_cut), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Copy", G_CALLBACK(action_copy), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Paste", G_CALLBACK(action_paste), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Delete", G_CALLBACK(action_delete), s)); + + 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("Find", G_CALLBACK(action_find), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Find Next", G_CALLBACK(action_find_next), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Replace", G_CALLBACK(action_replace), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Go To", G_CALLBACK(action_go_to), s)); + + 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("Select All", G_CALLBACK(action_select_all), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), menu_item("Time/Date", G_CALLBACK(action_time_date), s)); + + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), gtk_separator_menu_item_new()); + + // Backup toggle (you requested: option in Edit menu to turn off backup) + gtk_menu_shell_append(GTK_MENU_SHELL(edit_menu), + check_menu_item("Create .backup when unsaved", TRUE, G_CALLBACK(toggle_backups), s)); + + 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("Status Bar", TRUE, G_CALLBACK(toggle_status_bar), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), + check_menu_item("Word Wrap", FALSE, G_CALLBACK(toggle_word_wrap), s)); + gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), + check_menu_item("Line Numbering", TRUE, G_CALLBACK(toggle_line_numbers), s)); + + 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("About", G_CALLBACK(action_about), s)); + + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), help_root); + + return menubar; +} + +/* ---------- App init ---------- */ + +static void on_window_destroy(GtkWidget *w, gpointer user_data) { + (void)w; + AppState *s = (AppState*)user_data; + + 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); +} + +static void app_activate(GtkApplication *app, gpointer user_data) { + (void)user_data; + + AppState *s = g_new0(AppState, 1); + s->app = app; + + s->show_status_bar = TRUE; + s->word_wrap = FALSE; + s->show_line_numbers = TRUE; + s->backups_enabled = TRUE; + + s->win = gtk_application_window_new(app); + gtk_window_set_default_size(GTK_WINDOW(s->win), 900, 600); + + // 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); + + // Text editor + s->buffer = GTK_SOURCE_BUFFER(gtk_source_buffer_new(NULL)); + s->view = GTK_SOURCE_VIEW(gtk_source_view_new_with_buffer(s->buffer)); + gtk_source_view_set_show_line_numbers(s->view, TRUE); + + 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 box + 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); +} + +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; +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..e7d0c7c --- /dev/null +++ b/meson.build @@ -0,0 +1,10 @@ +project('supernotepad', 'c', version: '0.1', default_options: ['warning_level=2']) + +gtk = dependency('gtk+-3.0') +gtksource = dependency('gtksourceview-4') + +executable('SuperNotepad', + 'main.c', + dependencies: [gtk, gtksource], + install: true +)