diff --git a/Makefile b/Makefile index ff52d29..67853ca 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm BIN := sovereign_browser -SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c +SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/history.c $(BIN): $(SRC) $(NOSTR_LIB) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS) diff --git a/VERSION b/VERSION index 4e379d2..bcab45a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.2 +0.0.3 diff --git a/src/history.c b/src/history.c new file mode 100644 index 0000000..f6e13df --- /dev/null +++ b/src/history.c @@ -0,0 +1,160 @@ +/* + * history.c — URL history tracking for sovereign_browser + */ + +#include "history.h" + +#include +#include +#include +#include +#include +#include +#include + +/* ── In-memory history ─────────────────────────────────────────── */ + +static char g_history[HISTORY_MAX_ENTRIES][HISTORY_URL_MAX_LEN]; +static int g_history_count = 0; + +/* ── Path helper ───────────────────────────────────────────────── */ + +static int history_path(char *out, size_t out_sz) { + const char *home = getenv("HOME"); + if (home == NULL || home[0] == '\0') { + return -1; + } + + char dir[512]; + int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home); + if (n < 0 || (size_t)n >= sizeof(dir)) { + return -1; + } + if (mkdir(dir, 0700) != 0 && errno != EEXIST) { + return -1; + } + + n = snprintf(out, out_sz, "%s/history.txt", dir); + if (n < 0 || (size_t)n >= out_sz) { + return -1; + } + return 0; +} + +/* ── Persistence ───────────────────────────────────────────────── */ + +void history_load(void) { + char path[512]; + if (history_path(path, sizeof(path)) != 0) { + return; + } + + FILE *f = fopen(path, "r"); + if (f == NULL) { + return; + } + + g_history_count = 0; + char line[HISTORY_URL_MAX_LEN]; + while (g_history_count < HISTORY_MAX_ENTRIES && + fgets(line, sizeof(line), f) != NULL) { + /* Strip newline. */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + len--; + } + if (len > 0) { + snprintf(g_history[g_history_count], HISTORY_URL_MAX_LEN, "%s", line); + g_history_count++; + } + } + fclose(f); +} + +static void history_save(void) { + char path[512]; + if (history_path(path, sizeof(path)) != 0) { + return; + } + + FILE *f = fopen(path, "w"); + if (f == NULL) { + return; + } + fchmod(fileno(f), 0600); + + /* Write in reverse order (most recent first) — same as in-memory order. */ + for (int i = 0; i < g_history_count; i++) { + fprintf(f, "%s\n", g_history[i]); + } + fclose(f); +} + +/* ── API ───────────────────────────────────────────────────────── */ + +void history_add(const char *url) { + if (url == NULL || url[0] == '\0') { + return; + } + + /* Skip sovereign:// internal pages. */ + if (strncmp(url, "sovereign://", 12) == 0) { + return; + } + + /* Check if the URL is already in the history. */ + int found_idx = -1; + for (int i = 0; i < g_history_count; i++) { + if (strcmp(g_history[i], url) == 0) { + found_idx = i; + break; + } + } + + if (found_idx >= 0) { + /* URL already exists — move it to the top. */ + if (found_idx == 0) { + return; /* already at the top, nothing to do */ + } + /* Shift items 0..found_idx-1 down by one. */ + char tmp[HISTORY_URL_MAX_LEN]; + snprintf(tmp, HISTORY_URL_MAX_LEN, "%s", g_history[found_idx]); + for (int i = found_idx; i > 0; i--) { + snprintf(g_history[i], HISTORY_URL_MAX_LEN, "%s", g_history[i - 1]); + } + snprintf(g_history[0], HISTORY_URL_MAX_LEN, "%s", tmp); + } else { + /* New URL — shift everything down by one to make room at the top. */ + if (g_history_count >= HISTORY_MAX_ENTRIES) { + g_history_count = HISTORY_MAX_ENTRIES - 1; + } + for (int i = g_history_count; i > 0; i--) { + snprintf(g_history[i], HISTORY_URL_MAX_LEN, "%s", g_history[i - 1]); + } + snprintf(g_history[0], HISTORY_URL_MAX_LEN, "%s", url); + g_history_count++; + } + + history_save(); +} + +int history_count(void) { + return g_history_count; +} + +const char *history_get(int index) { + if (index < 0 || index >= g_history_count) { + return NULL; + } + return g_history[index]; +} + +void history_clear(void) { + g_history_count = 0; + + char path[512]; + if (history_path(path, sizeof(path)) == 0) { + unlink(path); + } +} diff --git a/src/history.h b/src/history.h new file mode 100644 index 0000000..018ef55 --- /dev/null +++ b/src/history.h @@ -0,0 +1,53 @@ +/* + * history.h — URL history tracking for sovereign_browser + * + * Keeps a list of recently visited URLs (most recent first) and + * provides them for the History submenu. + */ + +#ifndef HISTORY_H +#define HISTORY_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define HISTORY_MAX_ENTRIES 50 +#define HISTORY_URL_MAX_LEN 2048 + +/* + * Add a URL to the history. If the URL is already the most recent + * entry, it's not duplicated. The list is capped at HISTORY_MAX_ENTRIES. + * Also persists to ~/.sovereign_browser/history.txt. + */ +void history_add(const char *url); + +/* + * Get the number of history entries. + */ +int history_count(void); + +/* + * Get a history entry by index (0 = most recent). + * Returns NULL if index is out of range. + */ +const char *history_get(int index); + +/* + * Load history from ~/.sovereign_browser/history.txt. + * Called once at startup. + */ +void history_load(void); + +/* + * Clear all history entries and delete the file. + */ +void history_clear(void); + +#ifdef __cplusplus +} +#endif + +#endif /* HISTORY_H */ diff --git a/src/login_dialog.c b/src/login_dialog.c index 29202cb..63ff9d1 100644 --- a/src/login_dialog.c +++ b/src/login_dialog.c @@ -20,6 +20,8 @@ #include "nostr_core/nip019.h" #include "nostr_core/nip046.h" #include "nostr_core/nsigner_transport.h" +#include "nostr_core/utils.h" +#include /* ── Helpers ──────────────────────────────────────────────────── */ @@ -72,12 +74,34 @@ static void privkey_to_hex(const unsigned char privkey[32], char hex[65]) { typedef struct { GtkWidget *dialog; GtkWidget *content_area; - GtkWidget *stack; /* GtkStack for method switching */ + GtkWidget *notebook; /* GtkNotebook for method tabs */ GtkWidget *status_label; /* error/status display */ login_result_t *result; /* output */ gboolean done; /* dialog completed */ } login_ctx_t; +/* Helper: get the method name of the currently selected tab. */ +static const char *get_current_method(login_ctx_t *ctx) { + gint page = gtk_notebook_get_current_page(GTK_NOTEBOOK(ctx->notebook)); + if (page < 0) return NULL; + GtkWidget *child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(ctx->notebook), page); + if (child == NULL) return NULL; + return (const char *)g_object_get_data(G_OBJECT(child), "method-name"); +} + +/* Helper: get the page widget for a given method name. */ +static GtkWidget *get_method_page(login_ctx_t *ctx, const char *method) { + gint n = gtk_notebook_get_n_pages(GTK_NOTEBOOK(ctx->notebook)); + for (gint i = 0; i < n; i++) { + GtkWidget *child = gtk_notebook_get_nth_page(GTK_NOTEBOOK(ctx->notebook), i); + const char *name = (const char *)g_object_get_data(G_OBJECT(child), "method-name"); + if (name && strcmp(name, method) == 0) { + return child; + } + } + return NULL; +} + /* ── Forward declarations ─────────────────────────────────────── */ static GtkWidget *create_local_screen(login_ctx_t *ctx); @@ -109,26 +133,41 @@ static void on_generate_key(GtkWidget *btn, gpointer user_data) { gtk_entry_set_text(GTK_ENTRY(entry), nsec); } +#define SEED_WORD_COUNT 12 + static void on_generate_mnemonic(GtkWidget *btn, gpointer user_data) { (void)btn; - GtkWidget *entry = GTK_WIDGET(user_data); + GtkWidget *box = GTK_WIDGET(user_data); char mnemonic[256] = {0}; unsigned char privkey[32], pubkey[32]; if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0, privkey, pubkey) != 0) { - gtk_entry_set_text(GTK_ENTRY(entry), "(generation failed)"); return; } - gtk_entry_set_text(GTK_ENTRY(entry), mnemonic); -} -/* Method selection button: switch the stack to the named screen. */ -static void on_method_button_clicked(GtkWidget *btn, gpointer user_data) { - GtkStack *stack = GTK_STACK(user_data); - const char *name = (const char *)g_object_get_data(G_OBJECT(btn), "stack-name"); - if (name) { - gtk_stack_set_visible_child_name(stack, name); + /* Split the mnemonic into words and fill the 12 entry boxes. */ + char *words[SEED_WORD_COUNT]; + int n = 0; + char *tok = strtok(mnemonic, " "); + while (tok != NULL && n < SEED_WORD_COUNT) { + words[n++] = tok; + tok = strtok(NULL, " "); + } + + for (int i = 0; i < n && i < SEED_WORD_COUNT; i++) { + char key[16]; + snprintf(key, sizeof(key), "word-%d", i); + GtkWidget *entry = g_object_get_data(G_OBJECT(box), key); + if (entry) { + gtk_entry_set_text(GTK_ENTRY(entry), words[i]); + } + } + + /* Focus the first word box. */ + GtkWidget *first = g_object_get_data(G_OBJECT(box), "word-0"); + if (first) { + gtk_widget_grab_focus(first); } } @@ -138,7 +177,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { login_ctx_t *ctx = (login_ctx_t *)user_data; (void)btn; - const char *current = gtk_stack_get_visible_child_name(GTK_STACK(ctx->stack)); + const char *current = get_current_method(ctx); if (current == NULL) { gtk_label_set_text(GTK_LABEL(ctx->status_label), "No method selected."); return; @@ -148,7 +187,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { /* ── Local key ─────────────────────────────────────────── */ if (strcmp(current, "local") == 0) { - GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "local"); + GtkWidget *screen = get_method_page(ctx, "local"); GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "nsec-entry"); const char *input = gtk_entry_get_text(GTK_ENTRY(entry)); @@ -207,18 +246,42 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { /* ── Seed phrase ───────────────────────────────────────── */ if (strcmp(current, "seed") == 0) { - GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "seed"); - GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "mnemonic-entry"); - const char *mnemonic = gtk_entry_get_text(GTK_ENTRY(entry)); + GtkWidget *screen = get_method_page(ctx, "seed"); + GtkWidget *acct_spin = g_object_get_data(G_OBJECT(screen), "account-spin"); + int account = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(acct_spin)); - if (mnemonic[0] == '\0') { + /* Concatenate the 12 word entries into a mnemonic string. */ + char mnemonic[512] = {0}; + int words_filled = 0; + for (int i = 0; i < SEED_WORD_COUNT; i++) { + char key[16]; + snprintf(key, sizeof(key), "word-%d", i); + GtkWidget *entry = g_object_get_data(G_OBJECT(screen), key); + if (entry) { + const char *word = gtk_entry_get_text(GTK_ENTRY(entry)); + if (word && word[0]) { + if (words_filled > 0) strcat(mnemonic, " "); + strcat(mnemonic, word); + words_filled++; + } + } + } + + if (words_filled == 0) { gtk_label_set_text(GTK_LABEL(ctx->status_label), - "Enter a seed phrase or click Generate."); + "Enter your seed phrase or click Generate."); + return; + } + if (words_filled != SEED_WORD_COUNT) { + char msg[64]; + snprintf(msg, sizeof(msg), + "Expected 12 words, got %d.", words_filled); + gtk_label_set_text(GTK_LABEL(ctx->status_label), msg); return; } unsigned char privkey[32], pubkey[32]; - if (nostr_derive_keys_from_mnemonic(mnemonic, 0, privkey, pubkey) != 0) { + if (nostr_derive_keys_from_mnemonic(mnemonic, account, privkey, pubkey) != 0) { gtk_label_set_text(GTK_LABEL(ctx->status_label), "Invalid seed phrase. Check the words and try again."); return; @@ -247,8 +310,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { memcpy(ctx->result->identity.pubkey_hex, pubkey_hex, 64); ctx->result->identity.pubkey_hex[64] = '\0'; privkey_to_hex(privkey, ctx->result->identity.privkey_hex); - strncpy(ctx->result->identity.mnemonic, mnemonic, sizeof(ctx->result->identity.mnemonic) - 1); - ctx->result->identity.mnemonic[sizeof(ctx->result->identity.mnemonic) - 1] = '\0'; + snprintf(ctx->result->identity.mnemonic, sizeof(ctx->result->identity.mnemonic), "%s", mnemonic); ctx->done = TRUE; gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_ACCEPT); @@ -257,7 +319,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { /* ── Read-only (npub) ──────────────────────────────────── */ if (strcmp(current, "readonly") == 0) { - GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "readonly"); + GtkWidget *screen = get_method_page(ctx, "readonly"); GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "npub-entry"); const char *input = gtk_entry_get_text(GTK_ENTRY(entry)); @@ -310,7 +372,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { /* ── NIP-46 remote signer ──────────────────────────────── */ if (strcmp(current, "nip46") == 0) { - GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "nip46"); + GtkWidget *screen = get_method_page(ctx, "nip46"); GtkWidget *entry = g_object_get_data(G_OBJECT(screen), "bunker-entry"); const char *bunker_url = gtk_entry_get_text(GTK_ENTRY(entry)); @@ -385,7 +447,7 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { /* ── n_signer hardware ─────────────────────────────────── */ if (strcmp(current, "nsigner") == 0) { #if defined(NOSTR_ENABLE_NSIGNER_CLIENT) - GtkWidget *screen = gtk_stack_get_child_by_name(GTK_STACK(ctx->stack), "nsigner"); + GtkWidget *screen = get_method_page(ctx, "nsigner"); GtkWidget *transport_combo = g_object_get_data(G_OBJECT(screen), "transport-combo"); GtkWidget *device_entry = g_object_get_data(G_OBJECT(screen), "device-entry"); GtkWidget *index_spin = g_object_get_data(G_OBJECT(screen), "index-spin"); @@ -394,6 +456,14 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { int nostr_index = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(index_spin)); gint transport_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(transport_combo)); + /* Block SIGCHLD during n_signer calls — the qrexec transport spawns + * child processes, and SIGCHLD can interrupt gtk_dialog_run's + * internal main loop, causing the dialog to close prematurely. */ + sigset_t block_set, old_set; + sigemptyset(&block_set); + sigaddset(&block_set, SIGCHLD); + sigprocmask(SIG_BLOCK, &block_set, &old_set); + if (device[0] == '\0') { gtk_label_set_text(GTK_LABEL(ctx->status_label), "Enter a device path or select one."); @@ -411,7 +481,6 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { signer = nostr_signer_nsigner_unix(device, NULL, 15000); transport_name = "unix"; } else if (transport_idx == 2) { - /* device is "host:port" */ char host[256]; int port = 0; const char *sep = strrchr(device, ':'); @@ -426,33 +495,53 @@ static void on_login_clicked(GtkWidget *btn, gpointer user_data) { } } } else if (transport_idx == 3) { - signer = nostr_signer_nsigner_qrexec(device, "qubes.NsignerRpc", NULL, 30000); + GtkWidget *service_entry = g_object_get_data(G_OBJECT(screen), "service-entry"); + const char *service = service_entry ? gtk_entry_get_text(GTK_ENTRY(service_entry)) : "qubes.NsignerRpc"; + if (service[0] == '\0') service = "qubes.NsignerRpc"; + signer = nostr_signer_nsigner_qrexec(device, service, NULL, 30000); transport_name = "qrexec"; } + if (signer == NULL) { gtk_label_set_text(GTK_LABEL(ctx->status_label), - "Failed to connect to n_signer. Check the device/path."); + "Failed to connect to n_signer. Check the device/path/qube."); + sigprocmask(SIG_SETMASK, &old_set, NULL); return; } - /* Set the nostr_index. */ - if (nostr_signer_nsigner_set_nostr_index(signer, nostr_index) != NOSTR_SUCCESS) { + int rc_idx = nostr_signer_nsigner_set_nostr_index(signer, nostr_index); + if (rc_idx != NOSTR_SUCCESS) { gtk_label_set_text(GTK_LABEL(ctx->status_label), "Failed to set nostr_index on n_signer."); nostr_signer_free(signer); + sigprocmask(SIG_SETMASK, &old_set, NULL); return; } - /* Get the pubkey to verify the connection. */ char pubkey_hex[65]; - if (nostr_signer_get_public_key(signer, pubkey_hex) != NOSTR_SUCCESS) { - gtk_label_set_text(GTK_LABEL(ctx->status_label), - "Failed to get pubkey from n_signer. Is the device unlocked?"); - nostr_signer_free(signer); + int rc_pk = nostr_signer_get_public_key(signer, pubkey_hex); + if (rc_pk != NOSTR_SUCCESS) { + char errmsg[256]; + const char *desc = "unknown error"; + switch (rc_pk) { + case -5: desc = "I/O failed — signer may have denied the request or disconnected"; break; + case -2001: desc = "policy denied — caller not approved at signer terminal"; break; + case -2002: desc = "index not in signer's whitelist"; break; + case -3: desc = "crypto operation failed"; break; + default: break; + } + snprintf(errmsg, sizeof(errmsg), + "n_signer error: %s (code %d). Try a different key index.", + desc, rc_pk); + gtk_label_set_text(GTK_LABEL(ctx->status_label), errmsg); + sigprocmask(SIG_SETMASK, &old_set, NULL); return; } + /* Unblock SIGCHLD — n_signer calls are done. */ + sigprocmask(SIG_SETMASK, &old_set, NULL); + char npub[128]; pubkey_to_npub(pubkey_hex, npub); g_print("[login] n_signer: transport=%s index=%d pubkey=%s npub=%s\n", @@ -495,6 +584,169 @@ static void on_cancel_clicked(GtkWidget *btn, gpointer user_data) { gtk_dialog_response(GTK_DIALOG(ctx->dialog), GTK_RESPONSE_CANCEL); } +/* ── BIP-39 inline auto-complete ──────────────────────────────── */ + +/* State to prevent recursive signal handling during auto-fill. */ +static int g_autofill_suppress = 0; + +/* Track the last key pressed to detect deletions (skip auto-fill on delete). */ +static int g_last_key_was_delete = 0; + +/* Idle callback to set cursor position after auto-fill (deferred so GTK + * doesn't override our cursor placement). */ +static gboolean deferred_set_cursor(gpointer data) { + GtkEditable *editable = GTK_EDITABLE(data); + gpointer pos_ptr = g_object_get_data(G_OBJECT(editable), "target-pos"); + if (pos_ptr) { + gtk_editable_set_position(editable, GPOINTER_TO_INT(pos_ptr)); + g_object_set_data(G_OBJECT(editable), "target-pos", NULL); + } + g_object_unref(editable); + return FALSE; /* one-shot */ +} + +/* Called when a word entry changes — auto-fills if unique match found. */ +static void on_word_changed(GtkEditable *editable, gpointer user_data) { + (void)user_data; + if (g_autofill_suppress) return; + if (g_last_key_was_delete) { + g_last_key_was_delete = 0; + return; + } + + const char *text = gtk_entry_get_text(GTK_ENTRY(editable)); + if (text[0] == '\0') return; + + size_t text_len = strlen(text); + + /* Search the BIP-39 wordlist for matches. */ + int word_count = 0; + const char * const *words = nostr_bip39_get_wordlist(&word_count); + const char *match = NULL; + int match_count = 0; + + for (int i = 0; i < word_count; i++) { + if (strncmp(words[i], text, text_len) == 0) { + match = words[i]; + match_count++; + if (match_count > 1) break; /* no unique match */ + } + } + + /* If exactly one match and it's longer than what's typed, auto-fill. */ + if (match_count == 1 && match && strlen(match) > text_len) { + g_autofill_suppress = 1; + /* Set the full word. */ + char full[32]; + snprintf(full, sizeof(full), "%s", match); + gtk_entry_set_text(GTK_ENTRY(editable), full); + /* Place cursor at the end — deferred so GTK doesn't override it. */ + gint end_pos = (gint)strlen(full); + g_object_set_data(G_OBJECT(editable), "target-pos", + GINT_TO_POINTER(end_pos)); + g_idle_add_full(G_PRIORITY_HIGH_IDLE, deferred_set_cursor, + g_object_ref(editable), NULL); + g_autofill_suppress = 0; + } else if (match_count == 1 && match && strlen(match) == text_len) { + /* Exact match (e.g. selected from dropdown) — cursor to end. */ + gtk_editable_set_position(editable, (gint)text_len); + } +} + +/* Called on key press in a word entry — Tab/Enter moves to next box, + * and tracks BackSpace/Delete to skip auto-fill. */ +static gboolean on_word_key_press(GtkWidget *widget, GdkEventKey *event, + gpointer user_data) { + GtkWidget **entries = (GtkWidget **)user_data; + + /* Track deletions so on_word_changed skips auto-fill. */ + if (event->keyval == GDK_KEY_BackSpace || event->keyval == GDK_KEY_Delete) { + g_last_key_was_delete = 1; + } + + if (event->keyval == GDK_KEY_Tab || event->keyval == GDK_KEY_Return || + event->keyval == GDK_KEY_KP_Enter) { + /* Find the current entry index and focus the next one. */ + int i; + for (i = 0; i < SEED_WORD_COUNT; i++) { + if (entries[i] == widget) break; + } + if (i < SEED_WORD_COUNT - 1) { + gtk_widget_grab_focus(entries[i + 1]); + } + return TRUE; /* stop propagation */ + } + return FALSE; +} + +/* Update the derivation path hint when the account number changes. */ +static void on_account_changed(GtkSpinButton *spin, gpointer user_data) { + GtkWidget *hint = GTK_WIDGET(user_data); + int account = gtk_spin_button_get_value_as_int(spin); + char text[64]; + snprintf(text, sizeof(text), + "Key derivation: m/44'/1237'/%d'/0/0", account); + gtk_label_set_text(GTK_LABEL(hint), text); +} + +/* Update the n_signer UI when the transport type changes. */ +static void on_transport_changed(GtkComboBox *combo, gpointer user_data) { + /* user_data is the box; we retrieve the widgets from it. */ + GtkWidget *box = GTK_WIDGET(user_data); + GtkWidget *device_label = g_object_get_data(G_OBJECT(box), "device-label"); + GtkWidget *device_entry = g_object_get_data(G_OBJECT(box), "device-entry"); + GtkWidget *enum_btn = g_object_get_data(G_OBJECT(box), "enum-btn"); + GtkWidget *service_box = g_object_get_data(G_OBJECT(box), "service-box"); + + gint idx = gtk_combo_box_get_active(combo); + + switch (idx) { + case 0: /* USB Serial */ + gtk_label_set_text(GTK_LABEL(device_label), "Device Path:"); + gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "/dev/ttyACM0"); + /* Clear any default text from qrexec mode. */ + if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 && + strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) { + gtk_entry_set_text(GTK_ENTRY(device_entry), ""); + } + gtk_widget_show(enum_btn); + gtk_widget_hide(service_box); + break; + case 1: /* UNIX Socket */ + gtk_label_set_text(GTK_LABEL(device_label), "Socket Name:"); + gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nsigner"); + if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 && + strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) { + gtk_entry_set_text(GTK_ENTRY(device_entry), ""); + } + gtk_widget_hide(enum_btn); + gtk_widget_hide(service_box); + break; + case 2: /* TCP */ + gtk_label_set_text(GTK_LABEL(device_label), "Host:Port:"); + gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "127.0.0.1:7777"); + if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) > 0 && + strcmp(gtk_entry_get_text(GTK_ENTRY(device_entry)), "nostr_signer") == 0) { + gtk_entry_set_text(GTK_ENTRY(device_entry), ""); + } + gtk_widget_hide(enum_btn); + gtk_widget_hide(service_box); + break; + case 3: /* Qubes qrexec */ + gtk_label_set_text(GTK_LABEL(device_label), "Target Qube:"); + gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nostr_signer"); + /* Set default value so the user doesn't need to type it. */ + if (gtk_entry_get_text_length(GTK_ENTRY(device_entry)) == 0) { + gtk_entry_set_text(GTK_ENTRY(device_entry), "nostr_signer"); + } + gtk_widget_hide(enum_btn); + gtk_widget_show(service_box); + break; + default: + break; + } +} + /* ── Screen creation ──────────────────────────────────────────── */ static GtkWidget *create_local_screen(login_ctx_t *ctx) { @@ -530,25 +782,98 @@ static GtkWidget *create_seed_screen(login_ctx_t *ctx) { gtk_widget_set_margin_start(box, 12); gtk_widget_set_margin_end(box, 12); - GtkWidget *label = gtk_label_new("Enter a BIP-39 seed phrase (12-24 words) or generate a new one:"); + GtkWidget *label = gtk_label_new("Enter your 12-word BIP-39 seed phrase:"); gtk_widget_set_halign(label, GTK_ALIGN_START); gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 0); - GtkWidget *entry = gtk_entry_new(); - gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "abandon abandon abandon ... abandon art"); - gtk_entry_set_width_chars(GTK_ENTRY(entry), 60); - gtk_box_pack_start(GTK_BOX(box), entry, FALSE, FALSE, 0); + /* Build the BIP-39 word completion model (shared by all 12 entries). */ + GtkListStore *store = gtk_list_store_new(1, G_TYPE_STRING); + int word_count = 0; + const char * const *words = nostr_bip39_get_wordlist(&word_count); + for (int i = 0; i < word_count; i++) { + GtkTreeIter iter; + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, 0, words[i], -1); + } + + /* 12 numbered word entry boxes in a 4x3 grid with inline auto-complete. */ + GtkWidget *grid = gtk_grid_new(); + gtk_grid_set_row_spacing(GTK_GRID(grid), 4); + gtk_grid_set_column_spacing(GTK_GRID(grid), 8); + gtk_box_pack_start(GTK_BOX(box), grid, FALSE, FALSE, 0); + + /* Allocate an array of entry pointers for Tab navigation. */ + GtkWidget **entries = g_new0(GtkWidget *, SEED_WORD_COUNT); + + for (int i = 0; i < SEED_WORD_COUNT; i++) { + int col = i % 4; + int row = i / 4; + + /* Number label. */ + char num[8]; + snprintf(num, sizeof(num), "%d.", i + 1); + GtkWidget *num_label = gtk_label_new(num); + gtk_widget_set_halign(num_label, GTK_ALIGN_END); + gtk_grid_attach(GTK_GRID(grid), num_label, col * 2, row, 1, 1); + + /* Word entry with dropdown completion + inline auto-complete. */ + GtkWidget *entry = gtk_entry_new(); + gtk_entry_set_width_chars(GTK_ENTRY(entry), 14); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "word"); + + /* Dropdown completion list. */ + GtkEntryCompletion *completion = gtk_entry_completion_new(); + gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(store)); + gtk_entry_completion_set_text_column(completion, 0); + gtk_entry_completion_set_minimum_key_length(completion, 2); + gtk_entry_set_completion(GTK_ENTRY(entry), completion); + g_object_unref(completion); + + /* Inline auto-complete on text change. */ + g_signal_connect(entry, "changed", G_CALLBACK(on_word_changed), NULL); + /* Tab/Enter moves to the next word box. */ + g_signal_connect(entry, "key-press-event", + G_CALLBACK(on_word_key_press), entries); + + gtk_grid_attach(GTK_GRID(grid), entry, col * 2 + 1, row, 1, 1); + + entries[i] = entry; + + /* Store the entry on the box for later retrieval. */ + char key[16]; + snprintf(key, sizeof(key), "word-%d", i); + g_object_set_data(G_OBJECT(box), key, entry); + } + /* Store the entries array on the box (freed when box is destroyed). */ + g_object_set_data_full(G_OBJECT(box), "entries-array", entries, + (GDestroyNotify)g_free); + g_object_unref(store); GtkWidget *gen_btn = gtk_button_new_with_label("Generate New Mnemonic"); - g_signal_connect(gen_btn, "clicked", G_CALLBACK(on_generate_mnemonic), entry); + g_signal_connect(gen_btn, "clicked", G_CALLBACK(on_generate_mnemonic), box); gtk_box_pack_start(GTK_BOX(box), gen_btn, FALSE, FALSE, 0); - GtkWidget *hint = gtk_label_new("Key derivation: m/44'/1237'/0'/0/0 (account 0)"); + /* Account number selector. */ + GtkWidget *acct_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(box), acct_box, FALSE, FALSE, 0); + + GtkWidget *acct_label = gtk_label_new("Account #:"); + gtk_box_pack_start(GTK_BOX(acct_box), acct_label, FALSE, FALSE, 0); + + GtkWidget *acct_spin = gtk_spin_button_new_with_range(0, 1000, 1); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(acct_spin), 0); + gtk_box_pack_start(GTK_BOX(acct_box), acct_spin, FALSE, FALSE, 0); + + GtkWidget *hint = gtk_label_new("Key derivation: m/44'/1237'/0'/0/0"); gtk_widget_set_sensitive(hint, FALSE); gtk_widget_set_halign(hint, GTK_ALIGN_START); gtk_box_pack_start(GTK_BOX(box), hint, FALSE, FALSE, 0); - g_object_set_data(G_OBJECT(box), "mnemonic-entry", entry); + /* Update the hint when the account number changes. */ + g_signal_connect(acct_spin, "value-changed", + G_CALLBACK(on_account_changed), hint); + + g_object_set_data(G_OBJECT(box), "account-spin", acct_spin); return box; } @@ -633,8 +958,8 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { gtk_combo_box_set_active(GTK_COMBO_BOX(transport_combo), 0); gtk_box_pack_start(GTK_BOX(transport_box), transport_combo, FALSE, FALSE, 0); - /* Device path entry. */ - GtkWidget *device_label = gtk_label_new("Device / Path:"); + /* Device path entry — label and placeholder adapt to transport type. */ + GtkWidget *device_label = gtk_label_new("Device Path:"); gtk_widget_set_halign(device_label, GTK_ALIGN_START); gtk_box_pack_start(GTK_BOX(box), device_label, FALSE, FALSE, 0); @@ -643,7 +968,7 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { gtk_entry_set_width_chars(GTK_ENTRY(device_entry), 40); gtk_box_pack_start(GTK_BOX(box), device_entry, FALSE, FALSE, 0); - /* Enumerate serial devices button. */ + /* Enumerate serial devices button (only shown for serial transport). */ GtkWidget *enum_btn = gtk_button_new_with_label("Detect Serial Devices"); #if defined(NOSTR_ENABLE_NSIGNER_CLIENT) g_signal_connect(enum_btn, "clicked", G_CALLBACK(on_detect_serial), device_entry); @@ -652,6 +977,25 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { #endif gtk_box_pack_start(GTK_BOX(box), enum_btn, FALSE, FALSE, 0); + /* Service name field (only shown for qrexec transport). */ + GtkWidget *service_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_box_pack_start(GTK_BOX(box), service_box, FALSE, FALSE, 0); + + GtkWidget *service_label = gtk_label_new("Service:"); + gtk_box_pack_start(GTK_BOX(service_box), service_label, FALSE, FALSE, 0); + + GtkWidget *service_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(service_entry), "qubes.NsignerRpc"); + gtk_entry_set_width_chars(GTK_ENTRY(service_entry), 30); + gtk_box_pack_start(GTK_BOX(service_box), service_entry, FALSE, FALSE, 0); + + /* Initially hide the service field (serial is the default transport). */ + gtk_widget_hide(service_box); + + /* Connect transport change callback to update the UI dynamically. */ + g_signal_connect(transport_combo, "changed", + G_CALLBACK(on_transport_changed), box); + /* Nostr index spinner. */ GtkWidget *index_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); gtk_box_pack_start(GTK_BOX(box), index_box, FALSE, FALSE, 0); @@ -670,7 +1014,11 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) { gtk_box_pack_start(GTK_BOX(box), hint, FALSE, FALSE, 0); g_object_set_data(G_OBJECT(box), "transport-combo", transport_combo); + g_object_set_data(G_OBJECT(box), "device-label", device_label); g_object_set_data(G_OBJECT(box), "device-entry", device_entry); + g_object_set_data(G_OBJECT(box), "enum-btn", enum_btn); + g_object_set_data(G_OBJECT(box), "service-box", service_box); + g_object_set_data(G_OBJECT(box), "service-entry", service_entry); g_object_set_data(G_OBJECT(box), "index-spin", index_spin); return box; } @@ -706,15 +1054,24 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) { return -1; } - GtkWidget *dialog = gtk_dialog_new_with_buttons( - "sovereign browser — Sign In", - parent, - GTK_DIALOG_MODAL, - "_Cancel", GTK_RESPONSE_CANCEL, - "_Sign In", GTK_RESPONSE_ACCEPT, - NULL); + /* Create dialog WITHOUT buttons — we add custom buttons so we can + * control whether the dialog closes (the built-in buttons auto-respond + * and close the dialog before our handler can decide to keep it open). */ + /* Create dialog without auto-responding buttons — we add custom + * buttons so we control whether the dialog closes. */ + GtkWidget *dialog = gtk_dialog_new(); + gtk_window_set_title(GTK_WINDOW(dialog), "sovereign browser — Sign In"); + gtk_window_set_modal(GTK_WINDOW(dialog), TRUE); + if (parent) gtk_window_set_transient_for(GTK_WINDOW(dialog), parent); gtk_window_set_default_size(GTK_WINDOW(dialog), 560, 380); + /* Add custom buttons to the action area. */ + GtkWidget *action_area = gtk_dialog_get_action_area(GTK_DIALOG(dialog)); + GtkWidget *cancel_btn = gtk_button_new_with_label("_Cancel"); + GtkWidget *login_btn = gtk_button_new_with_label("_Sign In"); + gtk_box_pack_start(GTK_BOX(action_area), cancel_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(action_area), login_btn, FALSE, FALSE, 0); + /* Apply monospace font styling to the dialog. */ GtkCssProvider *css = gtk_css_provider_new(); gtk_css_provider_load_from_data(css, @@ -744,49 +1101,29 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) { "Sign in with your Nostr key"); gtk_box_pack_start(GTK_BOX(content), title, FALSE, FALSE, 8); - /* Method selector. */ - GtkWidget *method_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4); - gtk_box_pack_start(GTK_BOX(content), method_box, FALSE, FALSE, 4); + /* Notebook with tabs for each login method. */ + GtkWidget *notebook = gtk_notebook_new(); + gtk_box_pack_start(GTK_BOX(content), notebook, TRUE, TRUE, 4); + ctx.notebook = notebook; - /* Stack for method-specific screens. */ - GtkWidget *stack = gtk_stack_new(); - gtk_stack_set_transition_type(GTK_STACK(stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); - gtk_box_pack_start(GTK_BOX(content), stack, TRUE, TRUE, 4); - ctx.stack = stack; - - /* Create all screens. */ - GtkWidget *local_screen = create_local_screen(&ctx); - gtk_stack_add_named(GTK_STACK(stack), local_screen, "local"); - - GtkWidget *seed_screen = create_seed_screen(&ctx); - gtk_stack_add_named(GTK_STACK(stack), seed_screen, "seed"); - - GtkWidget *readonly_screen = create_readonly_screen(&ctx); - gtk_stack_add_named(GTK_STACK(stack), readonly_screen, "readonly"); - - GtkWidget *nip46_screen = create_nip46_screen(&ctx); - gtk_stack_add_named(GTK_STACK(stack), nip46_screen, "nip46"); - - GtkWidget *nsigner_screen = create_nsigner_screen(&ctx); - gtk_stack_add_named(GTK_STACK(stack), nsigner_screen, "nsigner"); - - /* Method buttons. */ + /* Create all screens as notebook tabs. */ const struct { const char *label; - const char *stack_name; - } methods[] = { - {"Local Key", "local"}, - {"Seed Phrase", "seed"}, - {"Read-only", "readonly"}, - {"NIP-46", "nip46"}, - {"n_signer", "nsigner"}, + const char *method; + GtkWidget *(*create_fn)(login_ctx_t *); + } tabs[] = { + {"Local Key", "local", create_local_screen}, + {"Seed Phrase", "seed", create_seed_screen}, + {"Read-only", "readonly", create_readonly_screen}, + {"NIP-46", "nip46", create_nip46_screen}, + {"n_signer", "nsigner", create_nsigner_screen}, }; - for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) { - GtkWidget *btn = gtk_button_new_with_label(methods[i].label); - g_object_set_data(G_OBJECT(btn), "stack-name", (gpointer)methods[i].stack_name); - g_signal_connect(btn, "clicked", G_CALLBACK(on_method_button_clicked), stack); - gtk_box_pack_start(GTK_BOX(method_box), btn, FALSE, FALSE, 0); + for (size_t i = 0; i < sizeof(tabs) / sizeof(tabs[0]); i++) { + GtkWidget *screen = tabs[i].create_fn(&ctx); + g_object_set_data(G_OBJECT(screen), "method-name", (gpointer)tabs[i].method); + GtkWidget *tab_label = gtk_label_new(tabs[i].label); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), screen, tab_label); } /* Status label. */ @@ -795,11 +1132,12 @@ int login_dialog_run(GtkWindow *parent, login_result_t *result) { gtk_box_pack_start(GTK_BOX(content), status, FALSE, FALSE, 4); ctx.status_label = status; - /* Wire the dialog buttons. */ - GtkWidget *cancel_btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL); - GtkWidget *login_btn = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + /* Wire the custom buttons — these don't auto-respond, so the dialog + * stays open unless we explicitly call gtk_dialog_response. */ g_signal_connect(cancel_btn, "clicked", G_CALLBACK(on_cancel_clicked), &ctx); g_signal_connect(login_btn, "clicked", G_CALLBACK(on_login_clicked), &ctx); + g_object_set_data(G_OBJECT(dialog), "login-btn", login_btn); + g_object_set_data(G_OBJECT(dialog), "cancel-btn", cancel_btn); gtk_widget_show_all(dialog); diff --git a/src/main.c b/src/main.c index d89ff2a..0ec98ab 100644 --- a/src/main.c +++ b/src/main.c @@ -19,12 +19,14 @@ #include #include +#include #include "version.h" #include "key_store.h" #include "login_dialog.h" #include "nostr_bridge.h" #include "nostr_inject.h" +#include "history.h" #include "nostr_core/nostr_core.h" /* ---- Global state --------------------------------------------------- * @@ -154,6 +156,69 @@ static void on_menu_inspector(GtkMenuItem *item, gpointer data) { webkit_web_inspector_show(inspector); } +/* History item callback: load the URL from the menu item label. */ +static void on_menu_history_item(GtkMenuItem *item, gpointer data) { + WebKitWebView *webview = WEBKIT_WEB_VIEW(data); + if (webview == NULL || item == NULL) return; + const char *url = gtk_menu_item_get_label(item); + if (url && url[0]) { + webkit_web_view_load_uri(webview, url); + } +} + +/* Clear history callback. */ +static void on_menu_history_clear(GtkMenuItem *item, gpointer data) { + (void)item; + (void)data; + history_clear(); + g_print("[history] cleared\n"); +} + +/* Rebuild the history submenu when it's shown (so it's always current). */ +static void on_history_menu_show(GtkWidget *menu, gpointer user_data) { + WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data); + + /* Remove all existing items. */ + GList *children = gtk_container_get_children(GTK_CONTAINER(menu)); + for (GList *l = children; l != NULL; l = l->next) { + gtk_widget_destroy(GTK_WIDGET(l->data)); + } + g_list_free(children); + + /* Rebuild with current history. */ + int hcount = history_count(); + if (hcount == 0) { + GtkWidget *empty = gtk_menu_item_new_with_label("(no recent pages)"); + gtk_widget_set_sensitive(empty, FALSE); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty); + } else { + int show = hcount < 20 ? hcount : 20; + for (int i = 0; i < show; i++) { + const char *url = history_get(i); + if (url == NULL) break; + char label[80]; + if (strlen(url) > 75) { + snprintf(label, sizeof(label), "%.72s...", url); + } else { + snprintf(label, sizeof(label), "%s", url); + } + GtkWidget *hitem = gtk_menu_item_new_with_label(label); + gtk_widget_set_tooltip_text(hitem, url); + g_signal_connect(hitem, "activate", + G_CALLBACK(on_menu_history_item), webview); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), hitem); + } + gtk_menu_shell_append(GTK_MENU_SHELL(menu), + gtk_separator_menu_item_new()); + GtkWidget *clear_item = gtk_menu_item_new_with_label("Clear Recents"); + g_signal_connect(clear_item, "activate", + G_CALLBACK(on_menu_history_clear), NULL); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item); + } + + gtk_widget_show_all(menu); +} + /* Open a local file via file chooser dialog. */ static void on_menu_open_file(GtkMenuItem *item, gpointer data) { (void)item; @@ -225,6 +290,18 @@ static GtkWidget *build_hamburger_menu(GtkWidget *window, gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload); gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_stop); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), + gtk_separator_menu_item_new()); + + /* Recents submenu — rebuilt dynamically each time it's shown. */ + GtkWidget *history_menu = gtk_menu_new(); + g_signal_connect(history_menu, "show", G_CALLBACK(on_history_menu_show), + webview); + + GtkWidget *item_history = gtk_menu_item_new_with_label("Recents"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_history), history_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_history); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); @@ -342,6 +419,10 @@ static void on_load_changed(WebKitWebView *webview, g_print("[loaded] %s -- title: %s\n", uri ? uri : "(null)", (title && title[0]) ? title : "(none)"); + /* Add to history on successful page load. */ + if (uri != NULL && uri[0] != '\0') { + history_add(uri); + } } } @@ -402,8 +483,15 @@ static int do_login(GtkWindow *parent) { /* ---- Main ------------------------------------------------------------ */ int main(int argc, char **argv) { + /* Ignore SIGPIPE — n_signer transports (qrexec, unix, tcp) may close + * pipes abruptly, and we don't want that to kill the browser. */ + (void)signal(SIGPIPE, SIG_IGN); + gtk_init(&argc, &argv); + /* Load URL history from disk. */ + history_load(); + const char *start_url = (argc > 1) ? argv[1] : "https://example.com"; char *normalized = normalize_url(start_url); @@ -415,9 +503,10 @@ int main(int argc, char **argv) { /* Login before showing the browser. */ if (do_login(GTK_WINDOW(window)) != 0) { - /* User cancelled login — exit. */ + /* User cancelled login — exit without gtk_main_quit (we haven't + * entered gtk_main yet, so calling gtk_main_quit would crash). */ g_print("[login] Cancelled, exiting.\n"); - gtk_widget_destroy(window); + nostr_cleanup(); return EXIT_SUCCESS; } diff --git a/src/nostr_bridge.c b/src/nostr_bridge.c index 0fd3fc8..6e7a5f2 100644 --- a/src/nostr_bridge.c +++ b/src/nostr_bridge.c @@ -339,8 +339,8 @@ static void handle_get_relays(WebKitURISchemeRequest *request) { /* ── Security settings page ────────────────────────────────────── */ -/* Generate the sovereign://security HTML page with toggle switches. */ -static void handle_security_page(WebKitURISchemeRequest *request) { +/* Generate the sovereign://settings HTML page. */ +static void handle_settings_page(WebKitURISchemeRequest *request) { /* Read current settings state. */ int dev_extras = g_bridge.settings ? webkit_settings_get_enable_developer_extras(g_bridge.settings) : 0; @@ -353,11 +353,12 @@ static void handle_security_page(WebKitURISchemeRequest *request) { char *html = g_strdup_printf( "\n" "\n" - "sovereign browser — Security Settings\n" + "sovereign browser — Settings\n" "\n" "\n" - "

Security Settings

\n" + "

sovereign browser — Settings

\n" + "\n" + "

Identity

\n" + "
Pubkey%.16s…
\n" + "
Method%s
\n" + "
Signing%s
\n" + "\n" + "

Security

\n" "

The 'reckless browser' thesis: identity and transport are handled\n" "at a different layer (Nostr keys + FIPS mesh), so the browser's own security\n" "sandbox is deliberately stripped. Toggle features on if you need them.

\n" @@ -418,7 +428,7 @@ static void handle_security_page(WebKitURISchemeRequest *request) { "\n" "\n" "\n", + g_bridge.pubkey_hex[0] ? g_bridge.pubkey_hex : "(none)", + g_bridge.readonly ? "read-only" : "signing", + g_bridge.signer ? "active" : (g_bridge.readonly ? "no signer" : "none"), dev_extras ? "on" : "off", file_access ? "on" : "off", universal_access ? "on" : "off" @@ -448,8 +461,8 @@ static void handle_security_page(WebKitURISchemeRequest *request) { g_free(html); } -/* Handle sovereign://security/set?feature=X — toggle a setting. */ -static void handle_security_set(WebKitURISchemeRequest *request, +/* Handle sovereign://settings/set?feature=X — toggle a setting. */ +static void handle_settings_set(WebKitURISchemeRequest *request, const char *query) { if (g_bridge.settings == NULL) { respond_error_json(request, -1, "Settings not available"); @@ -515,15 +528,25 @@ static void on_sovereign_scheme(WebKitURISchemeRequest *request, return; } - /* Route sovereign://security requests. */ - if (strcmp(uri, "sovereign://security") == 0 || - strncmp(uri, "sovereign://security?", 20) == 0) { - handle_security_page(request); + /* Route sovereign://settings requests. */ + if (strcmp(uri, "sovereign://settings") == 0 || + strncmp(uri, "sovereign://settings?", 21) == 0) { + handle_settings_page(request); return; } - if (strncmp(uri, "sovereign://security/set", 24) == 0) { + if (strncmp(uri, "sovereign://settings/set", 24) == 0) { const char *query = strchr(uri + 24, '?'); - handle_security_set(request, query ? query + 1 : ""); + handle_settings_set(request, query ? query + 1 : ""); + return; + } + + /* sovereign://security redirects to sovereign://settings for + * backward compatibility. */ + if (strcmp(uri, "sovereign://security") == 0) { + respond_html(request, + "" + "" + "Redirecting to settings…"); return; } diff --git a/src/version.h b/src/version.h index 8c3a568..8cead50 100644 --- a/src/version.h +++ b/src/version.h @@ -11,9 +11,9 @@ #ifndef SOVEREIGN_BROWSER_VERSION_H #define SOVEREIGN_BROWSER_VERSION_H -#define SB_VERSION "v0.0.2" +#define SB_VERSION "v0.0.3" #define SB_VERSION_MAJOR 0 #define SB_VERSION_MINOR 0 -#define SB_VERSION_PATCH 2 +#define SB_VERSION_PATCH 3 #endif /* SOVEREIGN_BROWSER_VERSION_H */