Compare commits

...
3 Commits
15 changed files with 1286 additions and 243 deletions
+1 -1
View File
@@ -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/history.c src/settings.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/history.c src/settings.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c src/profile.c
$(BIN): $(SRC) $(NOSTR_LIB)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS)
+1 -1
View File
@@ -1 +1 @@
0.0.18
0.0.21
+121
View File
@@ -0,0 +1,121 @@
# Per-User Profile Directory System
## Problem
When logging in as a new user (different nsec), the browser:
1. Restores the previous user's tabs (session restore from global SQLite)
2. Shows the previous user's Nostr events on the profile page
3. Shows the previous user's bookmarks, history, and recents
## Solution
Create a per-user profile directory keyed by npub, with each user getting their own SQLite database, session, bookmarks, and history.
## Directory Structure
```
~/.sovereign_browser/
├── identity.json # last used npub (for login dialog default)
├── global.db # global settings (agent server, theme, shortcuts, inspector)
└── profiles/
├── npub1abc.../
│ ├── browser.db # per-user: events, history, session, bookmarks, key_value
│ └── identity.json # this user's nsec/npub
├── npub1def.../
│ ├── browser.db
│ └── identity.json
└── ...
```
## What Stays Global vs Per-User
### Global (in `~/.sovereign_browser/global.db`)
- `agent_server_enabled`, `agent_server_port`, `agent_allowed_origins`, `agent_login_timeout_ms`
- `theme_dark`
- `shortcut.*` (keyboard shortcuts)
- `inspector_x/y/w/h`
- `dev_extras`, `file_access`, `universal_access` (security settings — these are WebKit-level, not per-user)
### Per-User (in `~/.sovereign_browser/profiles/<npub>/browser.db`)
- `restore_session`, `new_tab_url`, `tab_bar_position`, `show_tab_close_buttons`, `middle_click_close`, `ctrl_tab_switch`, `max_tabs`, `tab_drag_reorder`
- `bootstrap_relays`, `search_engine`
- Session (open tabs)
- History
- Bookmarks
- Nostr events (kind 0, 3, 10002)
- key_value table (per-user settings)
## Implementation Steps
### Step 1: Add `db_init_with_path(const char *path)` to db.c
Currently `db_init()` hardcodes `~/.sovereign_browser/browser.db`. Add a variant that takes a path. Keep `db_init()` for the global database.
### Step 2: Add profile directory management
New functions in a `profile.c` module (or add to `key_store.c`):
- `profile_get_dir(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/`
- `profile_ensure_dir(const char *npub)` — creates the directory if it doesn't exist
- `profile_get_db_path(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/browser.db`
- `profile_get_identity_path(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/identity.json`
### Step 3: Split settings into global and per-user
In `settings.c`:
- `settings_load_global()` — loads from global.db (agent server, theme, shortcuts, inspector, security)
- `settings_load_user()` — loads from the per-user browser.db (session, tabs, relays, search)
- `settings_save_global()` — saves global settings to global.db
- `settings_save_user()` — saves per-user settings to browser.db
Or simpler: keep one `settings_load()` / `settings_save()` but have it read from the appropriate database based on which keys are global vs per-user.
### Step 4: Change the login flow in main.c
Current flow:
1. `db_init()` — opens global browser.db
2. `settings_load()` — loads all settings from browser.db
3. `shortcuts_load()` — loads shortcuts from browser.db
4. Login (GTK dialog or agent login or CLI)
5. `relay_fetch()` — fetches events from relays
6. `session_restore()` — restores tabs from browser.db
New flow:
1. `db_init_global()` — opens global.db for global settings
2. `settings_load_global()` — loads global settings (agent server, theme, shortcuts, inspector)
3. `shortcuts_load()` — loads shortcuts from global.db
4. Login — get npub
5. `profile_ensure_dir(npub)` — create `~/.sovereign_browser/profiles/<npub>/`
6. `db_init_user(npub)` — close global.db, open `profiles/<npub>/browser.db`
7. `settings_load_user()` — load per-user settings from browser.db
8. `relay_fetch()` — fetch events from relays into per-user browser.db
9. `session_restore()` — restore tabs from per-user browser.db
### Step 5: Migration
On first run with the new system:
- If `~/.sovereign_browser/browser.db` exists and `~/.sovereign_browser/profiles/` doesn't:
- After login, copy `browser.db` to `profiles/<npub>/browser.db`
- Move global settings from browser.db to global.db
- Keep browser.db as backup (or rename to browser.db.old)
### Step 6: Identity persistence
- `~/.sovereign_browser/identity.json` — stores the last-used npub (not nsec)
- `~/.sovereign_browser/profiles/<npub>/identity.json` — stores the nsec/npub for that profile
- On startup, read the last-used npub from global identity.json to pre-fill the login dialog
## Files to Modify
1. `src/db.c` / `src/db.h` — add `db_init_with_path()`, `db_close()`, `db_init_global()`
2. `src/settings.c` / `src/settings.h` — split into global/per-user load/save
3. `src/main.c` — change the startup flow
4. `src/key_store.c` — update identity persistence to per-profile
5. `src/session.c` — no change (already uses db_session_* which will use the per-user db)
6. `src/shortcuts.c` — load from global.db instead of browser.db
7. New: `src/profile.c` / `src/profile.h` — profile directory management
## Risk Assessment
- **Medium risk** — changes the database initialization flow which affects all data access
- **Migration needed** — existing users need their data moved to per-profile dirs
- **Testing needed** — login with different users, verify data isolation
+130 -13
View File
@@ -24,24 +24,44 @@ static sqlite3 *g_db = NULL;
/* Static buffer for db_kv_get() — valid until the next call. */
static char g_kv_buf[4096];
/* ── Path helper ──────────────────────────────────────────────────── */
/* ── Path helpers ──────────────────────────────────────────────────── */
static int db_path(char *out, size_t out_sz) {
/* ~/.sovereign_browser/ (created if missing). Returns 0 on success. */
static int sb_home_dir(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
return -1;
}
int n = snprintf(out, out_sz, "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
if (mkdir(out, 0700) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* ~/.sovereign_browser/browser.db (legacy default per-user path). */
static int db_path(char *out, size_t out_sz) {
char dir[512];
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= sizeof(dir)) {
if (sb_home_dir(dir, sizeof(dir)) != 0) {
return -1;
}
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
int n = snprintf(out, out_sz, "%s/browser.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
return 0;
}
n = snprintf(out, out_sz, "%s/browser.db", dir);
/* ~/.sovereign_browser/global.db (global settings + shortcuts). */
static int db_global_path(char *out, size_t out_sz) {
char dir[512];
if (sb_home_dir(dir, sizeof(dir)) != 0) {
return -1;
}
int n = snprintf(out, out_sz, "%s/global.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
@@ -101,20 +121,29 @@ static const char *SCHEMA_SQL =
/* ── Init / Close ──────────────────────────────────────────────────── */
int db_init(void) {
char path[512];
if (db_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get database path\n");
/*
* Open a SQLite database at the given path with the standard schema.
* Closes any currently-open database first. Returns 0 on success.
*/
int db_init_with_path(const char *path) {
if (path == NULL || path[0] == '\0') {
g_printerr("[db] db_init_with_path: NULL path\n");
return -1;
}
/* Close any currently-open database so we can switch databases. */
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
}
int rc = sqlite3_open_v2(path, &g_db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] Failed to open database: %s\n",
sqlite3_errmsg(g_db));
g_printerr("[db] Failed to open database '%s': %s\n",
path, g_db ? sqlite3_errmsg(g_db) : "(null)");
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
@@ -134,7 +163,8 @@ int db_init(void) {
if (err) sqlite3_free(err);
}
/* Create schema. */
/* Create schema (all tables use IF NOT EXISTS, so this is safe for
* both global.db and per-user browser.db). */
rc = sqlite3_exec(g_db, SCHEMA_SQL, NULL, NULL, &err);
if (rc != SQLITE_OK) {
g_printerr("[db] Failed to create schema: %s\n",
@@ -149,6 +179,33 @@ int db_init(void) {
return 0;
}
/*
* Open the global database at ~/.sovereign_browser/global.db.
* Used at startup for global settings + shortcuts, before login.
*/
int db_init_global(void) {
char path[512];
if (db_global_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get global database path\n");
return -1;
}
return db_init_with_path(path);
}
/*
* Open the default per-user database at ~/.sovereign_browser/browser.db.
* Kept for compatibility; the normal flow now uses db_init_with_path()
* with a profile-specific path after login.
*/
int db_init(void) {
char path[512];
if (db_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get database path\n");
return -1;
}
return db_init_with_path(path);
}
void db_close(void) {
if (g_db) {
sqlite3_close(g_db);
@@ -394,6 +451,66 @@ int db_kv_set(const char *key, const char *value) {
return (rc == SQLITE_DONE) ? 0 : -1;
}
/*
* Write a key-value pair to a specific database file, opening a separate
* short-lived connection. This is used to save global settings to
* global.db while the per-user browser.db is the main open database.
* Creates the key_value table if it doesn't exist (safe for a fresh
* global.db). Returns 0 on success, -1 on error.
*/
int db_kv_set_to_file(const char *path, const char *key, const char *value) {
if (path == NULL || key == NULL || value == NULL) return -1;
sqlite3 *db = NULL;
int rc = sqlite3_open_v2(path, &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: open '%s' failed: %s\n",
path, db ? sqlite3_errmsg(db) : "(null)");
if (db) sqlite3_close(db);
return -1;
}
sqlite3_busy_timeout(db, 5000);
/* Ensure the key_value table exists (harmless if already present). */
char *err = NULL;
rc = sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS key_value ("
" key TEXT PRIMARY KEY,"
" value TEXT"
");", NULL, NULL, &err);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: create table failed: %s\n",
err ? err : "(unknown)");
if (err) sqlite3_free(err);
sqlite3_close(db);
return -1;
}
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(db,
"INSERT OR REPLACE INTO key_value (key, value) VALUES (?, ?);",
-1, &stmt, NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: prepare failed: %s\n",
sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
sqlite3_close(db);
return (rc == SQLITE_DONE) ? 0 : -1;
}
const char *db_kv_get(const char *key) {
if (g_db == NULL || key == NULL) return NULL;
+31 -3
View File
@@ -28,14 +28,33 @@ extern "C" {
/*
* Initialize the database at ~/.sovereign_browser/browser.db.
* Creates tables and indexes if they don't exist.
* Call once at startup (after settings_load()).
* Kept for compatibility; prefer db_init_with_path() for per-user dbs.
*
* Returns 0 on success, -1 on error.
*/
int db_init(void);
/*
* Close the database. Call at shutdown.
* Initialize the global database at ~/.sovereign_browser/global.db.
* Used at startup for global settings + shortcuts, before login.
* Creates tables and indexes if they don't exist.
*
* Returns 0 on success, -1 on error.
*/
int db_init_global(void);
/*
* Open a SQLite database at the given path with the standard schema.
* Closes any currently-open database first (so you can switch from
* global.db to a per-user browser.db after login).
*
* Returns 0 on success, -1 on error.
*/
int db_init_with_path(const char *path);
/*
* Close the database. Call at shutdown, or before switching to a
* different database via db_init_with_path().
*/
void db_close(void);
@@ -79,11 +98,20 @@ int db_count_events(const char *pubkey_hex, int kind);
/* ── Key-Value store ───────────────────────────────────────────────── */
/*
* Set a key-value pair (upsert).
* Set a key-value pair (upsert) in the currently-open database.
* Returns 0 on success, -1 on error.
*/
int db_kv_set(const char *key, const char *value);
/*
* Set a key-value pair in a specific database file, opening a separate
* short-lived connection. Used to save global settings to global.db
* while the per-user browser.db is the main open database. Creates the
* key_value table if it doesn't exist.
* Returns 0 on success, -1 on error.
*/
int db_kv_set_to_file(const char *path, const char *key, const char *value);
/*
* Get a value by key.
* Returns a pointer to the value string, or NULL if not found.
+103
View File
@@ -9,9 +9,14 @@
* in-memory identity struct
* - key_store_delete_legacy_file() — defensively delete any
* identity.json left by a previous version that persisted keys
* - key_store_save_profile_identity() — save public identity info
* (method, pubkey_hex) to the per-profile identity.json
* - key_store_load_profile_identity() — load public identity info
* from the per-profile identity.json
*/
#include "key_store.h"
#include "profile.h"
#include <stdio.h>
#include <stdlib.h>
@@ -19,6 +24,8 @@
#include <unistd.h>
#include <errno.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "nostr_core/nostr_core.h"
#include "nostr_core/nip006.h"
#include "nostr_core/nip019.h"
@@ -109,3 +116,99 @@ int key_store_delete_legacy_file(void) {
}
return 0;
}
/* ── Per-profile identity persistence ──────────────────────────────── *
* The per-profile identity.json stores public identity information
* (method, pubkey_hex) so the browser can display profile info without
* requiring the user to log in first. Private keys are NEVER stored.
*/
int key_store_save_profile_identity(const char *pubkey_hex,
key_store_method_t method) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "pubkey_hex", pubkey_hex);
cJSON_AddNumberToObject(root, "method", (double)method);
/* Method name for human readability. */
const char *method_name = "none";
switch (method) {
case KEY_STORE_METHOD_NONE: method_name = "none"; break;
case KEY_STORE_METHOD_LOCAL: method_name = "local"; break;
case KEY_STORE_METHOD_SEED: method_name = "seed"; break;
case KEY_STORE_METHOD_READONLY: method_name = "readonly"; break;
case KEY_STORE_METHOD_NIP46: method_name = "nip46"; break;
case KEY_STORE_METHOD_NSIGNER: method_name = "nsigner"; break;
}
cJSON_AddStringToObject(root, "method_name", method_name);
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (json == NULL) {
return -1;
}
FILE *f = fopen(path, "w");
if (f == NULL) {
free(json);
return -1;
}
fputs(json, f);
fputc('\n', f);
fclose(f);
free(json);
return 0;
}
int key_store_load_profile_identity(const char *pubkey_hex,
key_store_method_t *method_out) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
return -1;
}
int rc = -1;
cJSON *pk = cJSON_GetObjectItemCaseSensitive(root, "pubkey_hex");
if (cJSON_IsString(pk) && strcmp(pk->valuestring, pubkey_hex) == 0) {
if (method_out) {
cJSON *m = cJSON_GetObjectItemCaseSensitive(root, "method");
if (cJSON_IsNumber(m)) {
*method_out = (key_store_method_t)m->valuedouble;
}
}
rc = 0;
}
cJSON_Delete(root);
return rc;
}
+18
View File
@@ -69,6 +69,24 @@ nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity);
*/
int key_store_delete_legacy_file(void);
/*
* Save public identity info (method, pubkey_hex) to the per-profile
* identity.json at ~/.sovereign_browser/profiles/<pubkey>/identity.json.
* Private keys are NEVER stored — only public information for display.
* Returns 0 on success, -1 on error.
*/
int key_store_save_profile_identity(const char *pubkey_hex,
key_store_method_t method);
/*
* Load public identity info from the per-profile identity.json.
* pubkey_hex — the expected pubkey (must match the file's pubkey)
* method_out — if non-NULL, receives the stored login method
* Returns 0 on success, -1 if not found or pubkey mismatch.
*/
int key_store_load_profile_identity(const char *pubkey_hex,
key_store_method_t *method_out);
#ifdef __cplusplus
}
#endif
+134 -13
View File
@@ -47,6 +47,7 @@
#include "agent_login.h"
#include "cli.h"
#include "db.h"
#include "profile.h"
#include "relay_fetch.h"
#include "bookmarks.h"
#include "nostr_core/nostr_core.h"
@@ -65,6 +66,9 @@ typedef struct {
} app_state_t;
static app_state_t g_state = {0};
/* Forward declaration — defined before main(). */
static int switch_to_user_db(const char *pubkey_hex);
static GtkWindow *g_window = NULL;
static gboolean g_logged_in = FALSE;
static gboolean g_is_fullscreen = FALSE; /* track fullscreen state (GTK3 has no getter */
@@ -85,6 +89,14 @@ void app_set_signer(nostr_signer_t *signer, const char *pubkey_hex,
/* Update modules that hold a signer reference. */
settings_sync_set_signer(signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* If this is the first login (we're still on global.db), switch to
* the per-user profile database. If we're already on a per-user db
* (e.g. switching identity at runtime), switch_to_user_db() will
* close it and open the new user's db. */
if (g_state.pubkey_hex[0] != '\0') {
switch_to_user_db(g_state.pubkey_hex);
}
}
void app_clear_signer(void) {
@@ -130,6 +142,13 @@ void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data) {
nostr_bridge_set_signer(g_state.signer, g_state.pubkey_hex,
g_state.readonly);
/* Switch to the new user's per-user profile database. This
* closes the current browser.db and opens the new user's
* browser.db, then loads their per-user settings. */
if (g_state.pubkey_hex[0] != '\0') {
switch_to_user_db(g_state.pubkey_hex);
}
g_print("[identity] switched: method=%d pubkey=%s\n",
g_state.method, g_state.pubkey_hex);
}
@@ -428,6 +447,78 @@ static int do_login(GtkWindow *parent) {
return 0;
}
/* ---- Per-user database switch --------------------------------------- *
* After login, switch from the global database to the per-user profile
* database. Creates the profile directory if needed, closes global.db,
* opens the per-user browser.db, and loads per-user settings.
*
* This function is called from:
* - app_set_signer() — when an agent logs in via MCP or CLI
* - app_menu_switch_identity_proxy() — runtime identity switch via menu
* - main() — after the GTK login dialog returns
*
* It is idempotent: if already on the correct per-user db, it's a no-op.
*/
/* Track the currently-open per-user db path so we can skip re-opening
* the same database (e.g. when app_set_signer() is called during the
* login dialog and then main() calls switch_to_user_db() again). */
static char g_current_profile_db[512] = "";
static int switch_to_user_db(const char *pubkey_hex) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
g_printerr("[profile] No pubkey, staying on global.db\n");
return -1;
}
/* Create the profile directory. */
if (profile_ensure_dir(pubkey_hex) != 0) {
g_printerr("[profile] Failed to create profile dir for %s\n",
pubkey_hex);
return -1;
}
/* Open the per-user browser.db (closes global.db first). */
char db_path[512];
profile_get_db_path(pubkey_hex, db_path, sizeof(db_path));
if (db_path[0] == '\0') {
g_printerr("[profile] Failed to get db path for %s\n", pubkey_hex);
return -1;
}
/* Skip if already on this database (idempotent). */
if (g_current_profile_db[0] != '\0' &&
strcmp(g_current_profile_db, db_path) == 0) {
g_print("[profile] Already on per-user db: %s\n", db_path);
return 0;
}
if (db_init_with_path(db_path) != 0) {
g_printerr("[profile] Failed to open per-user db: %s\n", db_path);
return -1;
}
/* Record the current profile db path (for idempotency check). */
snprintf(g_current_profile_db, sizeof(g_current_profile_db),
"%s", db_path);
/* Load per-user settings from the per-user browser.db. This only
* reads per-user keys; global settings already in memory are
* preserved. */
settings_load_user();
/* Save the last-used pubkey to the global identity.json so the
* login dialog can default to this profile next time. */
profile_save_last_pubkey(pubkey_hex);
/* Save public identity info (method, pubkey) to the per-profile
* identity.json. Private keys are never stored. */
key_store_save_profile_identity(pubkey_hex, g_state.method);
g_print("[profile] Switched to per-user db: %s\n", db_path);
return 0;
}
/* ---- Main ----------------------------------------------------------- */
int main(int argc, char **argv) {
@@ -454,20 +545,34 @@ int main(int argc, char **argv) {
gtk_init(&argc, &argv);
/* Defensively delete any legacy identity.json from a previous version
* that persisted private keys to disk. Keys are now in-memory only. */
key_store_delete_legacy_file();
/* NOTE: key_store_delete_legacy_file() was previously called here
* to delete identity.json from old versions that persisted private
* keys. We no longer call it because identity.json is now used
* legitimately to store the last-used pubkey_hex (no private keys).
* This is a new project with no legacy files to migrate. */
/* Initialize the SQLite database first — settings and history are
* stored there. */
db_init();
/* Initialize the global database first — global settings and
* shortcuts are stored there. The per-user browser.db is opened
* after login, once we know the user's pubkey. */
db_init_global();
/* Load settings from the database (key_value table). */
settings_load();
/* Set defaults, then load global settings from global.db. Per-user
* settings are loaded after login from the per-user browser.db. */
{
browser_settings_t *s = settings_get_mutable();
/* settings_load_global() does NOT reset defaults, so we need to
* set them first. We call settings_load() which sets defaults
* and then loads global settings from the currently-open db
* (global.db). The per-user keys won't be found in global.db,
* so their defaults are kept. */
(void)s;
settings_load();
}
/* Load keyboard shortcut bindings from the database. Must come after
* settings_load() because shortcuts_lookup() checks the master
* ctrl_tab_switch toggle. */
/* Load keyboard shortcut bindings from the database (global.db).
* Must come after settings_load() because shortcuts_lookup() checks
* the master ctrl_tab_switch toggle. Shortcuts are global — they
* are the same across all profiles. */
shortcuts_load();
/* History is queried from the SQLite database on demand — no
@@ -590,10 +695,26 @@ int main(int argc, char **argv) {
}
}
/* ── Switch to per-user profile database ────────────────────── *
* Now that we know the user's pubkey, close global.db and open the
* per-user browser.db at ~/.sovereign_browser/profiles/<pubkey>/.
* This must happen before relay_fetch, session_restore, bookmarks,
* etc. — all of which read/write the per-user database.
*
* If there's no pubkey (--no-login mode), we stay on global.db.
* Browsing still works; history/session just go to global.db in
* that case (acceptable for the no-identity mode). */
if (g_state.pubkey_hex[0] != '\0') {
if (switch_to_user_db(g_state.pubkey_hex) != 0) {
g_printerr("[profile] Failed to switch to per-user db — "
"continuing with global.db\n");
}
}
/* If the user logged in with a Nostr identity (not --no-login), start
* a background thread to fetch their kind 0/3/10002 events from the
* bootstrap relays. The results are cached in the SQLite database.
* The thread frees the pubkey copy when done. */
* bootstrap relays. The results are cached in the per-user SQLite
* database. The thread frees the pubkey copy when done. */
if (g_state.pubkey_hex[0] != '\0') {
char *pubkey_copy = g_strdup(g_state.pubkey_hex);
g_thread_new("relay-fetch", relay_fetch_thread, pubkey_copy);
+30 -60
View File
@@ -473,6 +473,7 @@ static const char *sovereign_page_css(void) {
" border-radius: 4px; }\n"
" .theme-toggle { position: fixed; top: 20px; right: 20px;\n"
" display: flex; align-items: center; gap: 8px; cursor: pointer;\n"
" text-decoration: none; user-select: none;\n"
" font-size: 12px; color: var(--muted); user-select: none;\n"
" border: 1px solid var(--border); border-radius: var(--radius);\n"
" padding: 4px 10px; background: var(--bg); z-index: 10; }\n"
@@ -507,11 +508,10 @@ static char *sovereign_page_head(const char *title) {
* Returns a newly-allocated string (g_free). */
static char *sovereign_page_foot(void) {
return g_strdup_printf(
"<div class='theme-toggle' onclick='document.location.href="
"\"sovereign://settings/set?feature=theme_dark\"'>"
"%s &#9728;&#9790;</div>\n"
"<a class='theme-toggle' href='sovereign://settings/set?feature=theme_dark'>"
"%s</a>\n"
"</body></html>\n",
settings_get()->theme_dark ? "Day" : "Night");
settings_get()->theme_dark ? "Day Mode" : "Night Mode");
}
/* ── Settings page ─────────────────────────────────────────────── */
@@ -585,35 +585,29 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
/* Build the keyboard shortcuts section HTML. One row per action. */
GString *shortcuts_html = g_string_new(NULL);
g_string_append(shortcuts_html,
"<h2>Keyboard Shortcuts</h2>\n"
"<p class='note'>Click <b>Change</b> then press the key combination "
"you want. Press <b>Escape</b> to cancel.</p>\n");
"<h2>Keyboard Shortcuts</h2>\n");
for (int i = 0; i < SHORTCUT_COUNT; i++) {
const shortcut_meta_t *m = shortcuts_meta((shortcut_action_t)i);
const char *accel = shortcuts_get((shortcut_action_t)i);
char *label_esc = g_markup_escape_text(m->label, -1);
char *desc_esc = g_markup_escape_text(m->desc, -1);
char *accel_esc = g_markup_escape_text(accel ? accel : "", -1);
g_string_append_printf(shortcuts_html,
"<div class='field shortcut-row' data-action='%s' data-accel='%s'>\n"
" <div><div class='setting-name'>%s</div>\n"
" <div class='setting-desc'>%s</div></div>\n"
" <div><div class='setting-name'>%s</div></div>\n"
" <div><span class='shortcut-display' id='sc_%s'>%s</span>\n"
" <button class='save-btn sc-change' data-action='%s'>Change</button>\n"
" <button class='save-btn sc-reset' data-action='%s'>Reset</button></div>\n"
"</div>\n",
m->id, accel ? accel : "",
label_esc, desc_esc,
label_esc,
m->id, accel_esc,
m->id, m->id);
g_free(label_esc);
g_free(desc_esc);
g_free(accel_esc);
}
g_string_append(shortcuts_html,
"<div class='field'>"
"<div><div class='setting-name'>Reset all shortcuts</div>"
"<div class='setting-desc'>Restore all bindings to defaults</div></div>"
"<div><div class='setting-name'>Reset all shortcuts</div></div>"
"<div><button class='save-btn' id='sc-reset-all'>Reset All</button></div>"
"</div>\n");
@@ -627,29 +621,25 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Appearance</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Dark mode (Night)</div>\n"
" <div class='setting-desc'>Black background, white text. Toggle for Day mode.</div></div>\n"
" <div><div class='setting-name'>Dark mode (Night)</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"theme_dark\")'></div>\n"
"</div>\n"
"\n"
"<h2>Tabs</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Restore session on startup</div>\n"
" <div class='setting-desc'>Reopen tabs from the previous session</div></div>\n"
" <div><div class='setting-name'>Restore session on startup</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"restore_session\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>New tab URL</div>\n"
" <div class='setting-desc'>Page loaded when opening a new tab</div></div>\n"
" <div><div class='setting-name'>New tab URL</div></div>\n"
" <div><input type='text' id='new_tab_url' value='%s'>\n"
" <button class='save-btn' onclick=\"save('new_tab_url')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Tab bar position</div>\n"
" <div class='setting-desc'>Where the tab strip appears</div></div>\n"
" <div><div class='setting-name'>Tab bar position</div></div>\n"
" <div><select id='tab_bar_position'>\n"
" <option value='top'%s>Top</option>\n"
" <option value='bottom'%s>Bottom</option>\n"
@@ -660,32 +650,27 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Show tab close buttons</div>\n"
" <div class='setting-desc'>Per-tab close button in the tab strip</div></div>\n"
" <div><div class='setting-name'>Show tab close buttons</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"show_tab_close_buttons\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Middle-click to close tab</div>\n"
" <div class='setting-desc'>Middle-click on a tab closes it</div></div>\n"
" <div><div class='setting-name'>Middle-click to close tab</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"middle_click_close\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Ctrl+Tab to switch tabs</div>\n"
" <div class='setting-desc'>Cycle tabs with Ctrl+Tab / Ctrl+Shift+Tab</div></div>\n"
" <div><div class='setting-name'>Ctrl+Tab to switch tabs</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"ctrl_tab_switch\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Allow drag to reorder tabs</div>\n"
" <div class='setting-desc'>Drag tabs in the strip to reorder them</div></div>\n"
" <div><div class='setting-name'>Allow drag to reorder tabs</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"tab_drag_reorder\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Maximum tabs</div>\n"
" <div class='setting-desc'>Hard limit on simultaneous tabs</div></div>\n"
" <div><div class='setting-name'>Maximum tabs</div></div>\n"
" <div><input type='number' id='max_tabs' value='%d' min='1' max='999'>\n"
" <button class='save-btn' onclick=\"save('max_tabs')\">Save</button></div>\n"
"</div>\n"
@@ -695,38 +680,31 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Agent Server</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Enable agent server</div>\n"
" <div class='setting-desc'>MCP server for automation (takes effect on next launch)</div></div>\n"
" <div><div class='setting-name'>Enable agent server</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"agent_server_enabled\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Agent server port</div>\n"
" <div class='setting-desc'>TCP port for the MCP server (next launch)</div></div>\n"
" <div><div class='setting-name'>Agent server port</div></div>\n"
" <div><input type='number' id='agent_server_port' value='%d' min='1' max='65535'>\n"
" <button class='save-btn' onclick=\"save('agent_server_port')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Allowed origins</div>\n"
" <div class='setting-desc'>Comma-separated CORS origins (* for any)</div></div>\n"
" <div><div class='setting-name'>Allowed origins</div></div>\n"
" <div><input type='text' id='agent_allowed_origins' value='%s'>\n"
" <button class='save-btn' onclick=\"save('agent_allowed_origins')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Login timeout (ms)</div>\n"
" <div class='setting-desc'>Wait for agent login before GTK dialog</div></div>\n"
" <div><div class='setting-name'>Login timeout (ms)</div></div>\n"
" <div><input type='number' id='agent_login_timeout_ms' value='%d' min='0' max='300000'>\n"
" <button class='save-btn' onclick=\"save('agent_login_timeout_ms')\">Save</button></div>\n"
"</div>\n"
"\n"
"<h2>Bootstrap Relays</h2>\n"
"<p class='note'>Relays queried after login to fetch your profile (kind 0),\n"
"contacts (kind 3), and relay list (kind 10002). One wss:// URL per line.</p>\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Relay URLs</div>\n"
" <div class='setting-desc'>One wss:// URL per line</div></div>\n"
" <div><div class='setting-name'>Relay URLs</div></div>\n"
" <div><textarea id='bootstrap_relays' rows='4' cols='40'>%s</textarea>\n"
" <button class='save-btn' onclick=\"save('bootstrap_relays')\">Save</button></div>\n"
"</div>\n"
@@ -734,8 +712,7 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Search</h2>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Search engine</div>\n"
" <div class='setting-desc'>Engine used when typing a query in the URL bar</div></div>\n"
" <div><div class='setting-name'>Search engine</div></div>\n"
" <div><select id='search_engine'>\n"
"%s"
" </select>\n"
@@ -743,37 +720,29 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"</div>\n"
"\n"
"<h2>Security</h2>\n"
"<p class='note'>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.</p>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Developer Tools (Inspector)</div>\n"
" <div class='setting-desc'>Enable WebKit Web Inspector for JS debugging</div></div>\n"
" <div><div class='setting-name'>Developer Tools (Inspector)</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"dev_extras\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>File Access from file://</div>\n"
" <div class='setting-desc'>Allow file:// pages to access other file:// resources</div></div>\n"
" <div><div class='setting-name'>File Access from file://</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"file_access\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Universal Access from file://</div>\n"
" <div class='setting-desc'>Allow file:// pages to make cross-origin requests (needed for sovereign:// bridge)</div></div>\n"
" <div><div class='setting-name'>Universal Access from file://</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"universal_access\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>CORS Enforcement</div>\n"
" <div class='setting-desc'>Block cross-origin requests without CORS headers (currently always off)</div></div>\n"
" <div><div class='setting-name'>CORS Enforcement</div></div>\n"
" <div class='toggle off' style='opacity:0.5'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>TLS Certificate Check</div>\n"
" <div class='setting-desc'>Reject invalid/self-signed certificates (currently always off — FIPS uses Noise IK)</div></div>\n"
" <div><div class='setting-name'>TLS Certificate Check</div></div>\n"
" <div class='toggle off' style='opacity:0.5'></div>\n"
"</div>\n"
"\n"
@@ -996,7 +965,8 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
search_engine_options->str,
dev_extras ? "on" : "off",
file_access ? "on" : "off",
universal_access ? "on" : "off"
universal_access ? "on" : "off",
foot
);
respond_html(request, html);
+239
View File
@@ -0,0 +1,239 @@
/*
* profile.c — per-user profile directory management for sovereign_browser
*
* Each Nostr identity (identified by its 64-char hex pubkey) gets its own
* profile directory under ~/.sovereign_browser/profiles/<pubkey_hex>/.
*/
#include "profile.h"
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "../nostr_core_lib/cjson/cJSON.h"
/* ── Internal helpers ──────────────────────────────────────────────── */
/* Get ~/.sovereign_browser/ (created if missing). Returns 0 on success. */
static int sb_home_dir(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
return -1;
}
int n = snprintf(out, out_sz, "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
if (mkdir(out, 0700) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* Max path length we use for home-derived paths. The home directory
* can be long, and we append "/.sovereign_browser/profiles/<64-hex>/".
* 600 bytes is generous enough for any realistic HOME path. */
#define SB_HOME_BUF_SZ 600
/* mkdir -p style: create a directory and all parent directories. */
static int mkdir_p(const char *path, mode_t mode) {
char tmp[512];
int n = snprintf(tmp, sizeof(tmp), "%s", path);
if (n < 0 || (size_t)n >= sizeof(tmp)) {
return -1;
}
/* Remove trailing slash if present. */
size_t len = strlen(tmp);
if (len > 0 && tmp[len - 1] == '/') {
tmp[len - 1] = '\0';
}
/* Walk the path and create each component. */
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
return -1;
}
*p = '/';
}
}
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* ── Public API ────────────────────────────────────────────────────── */
void profile_get_dir(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char home[600];
if (sb_home_dir(home, sizeof(home)) != 0) {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%s/profiles/%s/", home, pubkey_hex);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
int profile_ensure_dir(const char *pubkey_hex) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
return -1;
}
return mkdir_p(dir, 0700);
}
void profile_get_db_path(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%sbrowser.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
void profile_get_identity_path(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%sidentity.json", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
void profile_get_global_identity_path(char *out, size_t out_sz) {
if (out == NULL || out_sz == 0) {
return;
}
char home[SB_HOME_BUF_SZ];
if (sb_home_dir(home, sizeof(home)) != 0) {
out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%s/identity.json", home);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
/* ── Last-used pubkey persistence (global identity.json) ───────────── */
int profile_save_last_pubkey(const char *pubkey_hex) {
char path[SB_HOME_BUF_SZ];
profile_get_global_identity_path(path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
cJSON *root = cJSON_CreateObject();
if (pubkey_hex && pubkey_hex[0]) {
cJSON_AddStringToObject(root, "pubkey_hex", pubkey_hex);
} else {
cJSON_AddNullToObject(root, "pubkey_hex");
}
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (json == NULL) {
return -1;
}
FILE *f = fopen(path, "w");
if (f == NULL) {
free(json);
return -1;
}
fputs(json, f);
fputc('\n', f);
fclose(f);
free(json);
g_print("[profile] Saved last-used pubkey: %s\n",
(pubkey_hex && pubkey_hex[0]) ? pubkey_hex : "(cleared)");
return 0;
}
int profile_load_last_pubkey(char *out, size_t out_sz) {
if (out == NULL || out_sz < 65) {
return -1;
}
char path[SB_HOME_BUF_SZ];
profile_get_global_identity_path(path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
/* Read the file (small JSON, a few hundred bytes at most). */
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
return -1;
}
cJSON *pk = cJSON_GetObjectItemCaseSensitive(root, "pubkey_hex");
const char *hex = cJSON_IsString(pk) ? pk->valuestring : NULL;
int rc = -1;
if (hex && strlen(hex) == 64) {
snprintf(out, out_sz, "%s", hex);
rc = 0;
}
cJSON_Delete(root);
return rc;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* profile.h — per-user profile directory management for sovereign_browser
*
* Each Nostr identity (identified by its 64-char hex pubkey) gets its own
* profile directory under ~/.sovereign_browser/profiles/<pubkey_hex>/
* containing a per-user browser.db and identity.json.
*
* Global settings (agent server, theme, shortcuts, inspector geometry)
* live in ~/.sovereign_browser/global.db and are shared across all users.
*/
#ifndef PROFILE_H
#define PROFILE_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Get the profile directory path for a given hex pubkey.
* pubkey_hex — 64-char hex pubkey (no leading "0x")
* out — output buffer
* out_sz — size of output buffer
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/" to out.
*/
void profile_get_dir(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Create the profile directory for the given hex pubkey if it doesn't
* already exist (mkdir -p style — creates parent dirs as needed).
* pubkey_hex — 64-char hex pubkey
* Returns 0 on success, -1 on error.
*/
int profile_ensure_dir(const char *pubkey_hex);
/*
* Get the per-user browser.db path for a given hex pubkey.
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/browser.db" to out.
*/
void profile_get_db_path(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Get the per-user identity.json path for a given hex pubkey.
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/identity.json" to out.
*/
void profile_get_identity_path(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Get the global identity.json path (stores the last-used pubkey_hex so
* the login dialog can default to the right profile).
* Writes "~/.sovereign_browser/identity.json" to out.
*/
void profile_get_global_identity_path(char *out, size_t out_sz);
/*
* Save the last-used pubkey_hex to the global identity.json.
* pubkey_hex — 64-char hex pubkey, or NULL/empty to clear
* Returns 0 on success, -1 on error.
*/
int profile_save_last_pubkey(const char *pubkey_hex);
/*
* Read the last-used pubkey_hex from the global identity.json.
* out — output buffer (must be at least 65 bytes)
* out_sz — size of output buffer
* Returns 0 on success (out filled with hex pubkey), -1 if not found or
* error (out is left unchanged).
*/
int profile_load_last_pubkey(char *out, size_t out_sz);
#ifdef __cplusplus
}
#endif
#endif /* PROFILE_H */
+166 -54
View File
@@ -15,6 +15,37 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
/* ── Global.db path helper ─────────────────────────────────────────── *
* Returns the path to ~/.sovereign_browser/global.db (creating the
* directory if needed). Used by settings_save_global() to write global
* settings to global.db via db_kv_set_to_file(), even when the per-user
* browser.db is the main open database.
*/
static void global_db_path(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
char dir[512];
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= sizeof(dir)) {
if (out_sz > 0) out[0] = '\0';
return;
}
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
if (out_sz > 0) out[0] = '\0';
return;
}
n = snprintf(out, out_sz, "%s/global.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
/* ── Global singleton ─────────────────────────────────────────────── */
@@ -96,13 +127,80 @@ static const char *position_to_string(int pos) {
}
}
/* ── Load / Save (SQLite key_value table) ─────────────────────────── */
/* ── Load / Save (SQLite key_value table) ─────────────────────────── *
* Settings are split into two groups:
*
* GLOBAL (stored in ~/.sovereign_browser/global.db):
* agent_server_enabled, agent_server_port, agent_allowed_origins,
* agent_login_timeout_ms, theme_dark, inspector_x/y/w/h
*
* PER-USER (stored in ~/.sovereign_browser/profiles/<pubkey>/browser.db):
* restore_session, new_tab_url, tab_bar_position,
* show_tab_close_buttons, middle_click_close, ctrl_tab_switch,
* max_tabs, tab_drag_reorder, bootstrap_relays, search_engine
*
* The settings struct is a single in-memory singleton. Global settings
* are loaded once at startup (from global.db) and kept in memory. Per-user
* settings are loaded after login (from the per-user browser.db). Both
* groups are saved when the user changes settings on the settings page.
*/
void settings_load(void) {
settings_set_defaults(&g_settings);
void settings_load_global(void) {
/* Read global settings from the currently-open database (global.db).
* Defaults must already be set (call settings_set_defaults first, or
* call settings_load_user() which sets defaults). */
const char *val;
/* Read each setting from the key_value table. If a key is missing
* (NULL), the default is kept. */
val = db_kv_get("agent_server_enabled");
if (val) g_settings.agent_server_enabled = parse_bool(val, g_settings.agent_server_enabled);
val = db_kv_get("agent_server_port");
if (val) {
g_settings.agent_server_port = parse_int(val, g_settings.agent_server_port);
if (g_settings.agent_server_port < 1) g_settings.agent_server_port = SETTINGS_AGENT_PORT_DEFAULT;
}
val = db_kv_get("agent_allowed_origins");
if (val) snprintf(g_settings.agent_allowed_origins, sizeof(g_settings.agent_allowed_origins), "%s", val);
val = db_kv_get("agent_login_timeout_ms");
if (val) {
g_settings.agent_login_timeout_ms = parse_int(val, g_settings.agent_login_timeout_ms);
if (g_settings.agent_login_timeout_ms < 0) g_settings.agent_login_timeout_ms = 0;
}
val = db_kv_get("theme_dark");
if (val) g_settings.theme_dark = parse_bool(val, g_settings.theme_dark);
val = db_kv_get("inspector_x");
if (val) g_settings.inspector_x = parse_int(val, g_settings.inspector_x);
val = db_kv_get("inspector_y");
if (val) g_settings.inspector_y = parse_int(val, g_settings.inspector_y);
val = db_kv_get("inspector_w");
if (val) g_settings.inspector_w = parse_int(val, g_settings.inspector_w);
val = db_kv_get("inspector_h");
if (val) g_settings.inspector_h = parse_int(val, g_settings.inspector_h);
}
void settings_load_user(void) {
/* Set defaults first — this resets the entire struct, so global
* settings loaded earlier would be lost. To preserve global settings,
* the caller should call settings_load_global() AFTER this function
* if both need to be (re)loaded. The normal startup flow is:
* 1. settings_set_defaults() (via settings_load_user)
* 2. settings_load_global() (from global.db)
* 3. ... login ...
* 4. settings_load_user() (from per-user browser.db, preserves
* global settings already in memory)
*
* But settings_load_user() does NOT reset defaults — it only reads
* per-user keys, leaving global keys untouched. So the correct flow
* is:
* 1. settings_set_defaults() — once
* 2. settings_load_global() — from global.db
* 3. ... login, switch to per-user db ...
* 4. settings_load_user() — from per-user browser.db
*/
const char *val;
val = db_kv_get("restore_session");
@@ -132,44 +230,67 @@ void settings_load(void) {
val = db_kv_get("tab_drag_reorder");
if (val) g_settings.tab_drag_reorder = parse_bool(val, g_settings.tab_drag_reorder);
val = db_kv_get("agent_server_enabled");
if (val) g_settings.agent_server_enabled = parse_bool(val, g_settings.agent_server_enabled);
val = db_kv_get("agent_server_port");
if (val) {
g_settings.agent_server_port = parse_int(val, g_settings.agent_server_port);
if (g_settings.agent_server_port < 1) g_settings.agent_server_port = SETTINGS_AGENT_PORT_DEFAULT;
}
val = db_kv_get("agent_allowed_origins");
if (val) snprintf(g_settings.agent_allowed_origins, sizeof(g_settings.agent_allowed_origins), "%s", val);
val = db_kv_get("agent_login_timeout_ms");
if (val) {
g_settings.agent_login_timeout_ms = parse_int(val, g_settings.agent_login_timeout_ms);
if (g_settings.agent_login_timeout_ms < 0) g_settings.agent_login_timeout_ms = 0;
}
val = db_kv_get("bootstrap_relays");
if (val) snprintf(g_settings.bootstrap_relays, sizeof(g_settings.bootstrap_relays), "%s", val);
val = db_kv_get("search_engine");
if (val) snprintf(g_settings.search_engine, sizeof(g_settings.search_engine), "%s", val);
val = db_kv_get("theme_dark");
if (val) g_settings.theme_dark = parse_bool(val, g_settings.theme_dark);
val = db_kv_get("inspector_x");
if (val) g_settings.inspector_x = parse_int(val, g_settings.inspector_x);
val = db_kv_get("inspector_y");
if (val) g_settings.inspector_y = parse_int(val, g_settings.inspector_y);
val = db_kv_get("inspector_w");
if (val) g_settings.inspector_w = parse_int(val, g_settings.inspector_w);
val = db_kv_get("inspector_h");
if (val) g_settings.inspector_h = parse_int(val, g_settings.inspector_h);
}
void settings_save(void) {
void settings_load(void) {
/* Backward-compatible full load: sets defaults, then loads all
* settings from the currently-open database. Used when a single
* database contains all settings (legacy or test scenarios). */
settings_set_defaults(&g_settings);
settings_load_global();
settings_load_user();
}
void settings_save_global(void) {
/* Save global settings to global.db. This uses db_kv_set_to_file()
* which opens a separate short-lived connection, so it works even
* when the per-user browser.db is the main open database (the normal
* case after login). At startup, when global.db IS the main open
* database, this also works (the separate connection is fine since
* SQLite handles concurrent readers/writers with FULLMUTEX). */
char gpath[512];
global_db_path(gpath, sizeof(gpath));
if (gpath[0] == '\0') {
g_printerr("[settings] Failed to get global.db path for save\n");
return;
}
char buf[32];
db_kv_set_to_file(gpath, "agent_server_enabled",
g_settings.agent_server_enabled ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.agent_server_port);
db_kv_set_to_file(gpath, "agent_server_port", buf);
db_kv_set_to_file(gpath, "agent_allowed_origins",
g_settings.agent_allowed_origins);
snprintf(buf, sizeof(buf), "%d", g_settings.agent_login_timeout_ms);
db_kv_set_to_file(gpath, "agent_login_timeout_ms", buf);
db_kv_set_to_file(gpath, "theme_dark",
g_settings.theme_dark ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_x);
db_kv_set_to_file(gpath, "inspector_x", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_y);
db_kv_set_to_file(gpath, "inspector_y", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_w);
db_kv_set_to_file(gpath, "inspector_w", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_h);
db_kv_set_to_file(gpath, "inspector_h", buf);
}
void settings_save_user(void) {
/* Save per-user settings to the currently-open database (should be
* the per-user browser.db). Called when the user changes per-user
* settings on the settings page. */
char buf[32];
db_kv_set("restore_session", g_settings.restore_session ? "true" : "false");
@@ -183,30 +304,21 @@ void settings_save(void) {
db_kv_set("max_tabs", buf);
db_kv_set("tab_drag_reorder", g_settings.tab_drag_reorder ? "true" : "false");
db_kv_set("agent_server_enabled", g_settings.agent_server_enabled ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.agent_server_port);
db_kv_set("agent_server_port", buf);
db_kv_set("agent_allowed_origins", g_settings.agent_allowed_origins);
snprintf(buf, sizeof(buf), "%d", g_settings.agent_login_timeout_ms);
db_kv_set("agent_login_timeout_ms", buf);
/* Bootstrap relays are stored as-is (newlines are fine in SQLite). */
db_kv_set("bootstrap_relays", g_settings.bootstrap_relays);
db_kv_set("search_engine", g_settings.search_engine);
db_kv_set("theme_dark", g_settings.theme_dark ? "true" : "false");
}
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_x);
db_kv_set("inspector_x", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_y);
db_kv_set("inspector_y", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_w);
db_kv_set("inspector_w", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_h);
db_kv_set("inspector_h", buf);
void settings_save(void) {
/* Backward-compatible full save: saves all settings to the
* currently-open database. The settings page should call
* settings_save_user() + settings_save_global() instead, so that
* global settings go to global.db and per-user settings go to the
* per-user browser.db. */
settings_save_user();
settings_save_global();
}
const browser_settings_t *settings_get(void) {
+36 -5
View File
@@ -15,7 +15,7 @@
extern "C" {
#endif
#define SETTINGS_NEW_TAB_URL_DEFAULT "https://example.com"
#define SETTINGS_NEW_TAB_URL_DEFAULT ""
#define SETTINGS_NEW_TAB_URL_MAX 512
#define SETTINGS_MAX_TABS_DEFAULT 50
#define SETTINGS_AGENT_PORT_DEFAULT 17777
@@ -52,17 +52,48 @@ typedef struct {
} browser_settings_t;
/*
* Load settings from disk into the global singleton.
* If the file doesn't exist or a key is missing, defaults are used.
* Call once at startup.
* Load all settings from the currently-open database into the global
* singleton. Sets defaults first, then reads all keys.
* Backward-compatible — prefer settings_load_global() + settings_load_user()
* for the split-database flow.
*/
void settings_load(void);
/*
* Save the current global settings to disk.
* Load global settings (agent server, theme, inspector geometry) from
* the currently-open database (should be global.db). Does NOT reset
* defaults — call settings_set_defaults() first (or use the full
* settings_load() if you need defaults reset).
*/
void settings_load_global(void);
/*
* Load per-user settings (tabs, relays, search engine) from the
* currently-open database (should be the per-user browser.db). Does NOT
* reset defaults — only reads per-user keys, leaving global keys in the
* struct untouched.
*/
void settings_load_user(void);
/*
* Save all settings to the currently-open database.
* Backward-compatible — prefer settings_save_global() + settings_save_user()
* for the split-database flow.
*/
void settings_save(void);
/*
* Save global settings to the currently-open database (should be
* global.db). Call when global settings change.
*/
void settings_save_global(void);
/*
* Save per-user settings to the currently-open database (should be the
* per-user browser.db). Call when per-user settings change.
*/
void settings_save_user(void);
/*
* Get a pointer to the global settings singleton.
* Always valid after settings_load().
+197 -91
View File
@@ -67,6 +67,25 @@ static GtkWindow *g_window = NULL;
static GtkWindow *g_active_window = NULL;
static GtkWidget *g_active_notebook = NULL;
/* Target notebook for the next tab_create() call.
*
* When non-NULL, tab_create()/tab_manager_new_tab() add the new tab to
* this notebook instead of g_notebook. Used by tab_manager_new_window()
* to place the first tab into the newly created window's notebook.
*
* When NULL, tab_manager_new_tab() falls back to g_active_notebook (the
* focused window's notebook) so Ctrl+T / "New Tab" opens in the active
* window rather than always the main window. */
static GtkWidget *g_target_notebook = NULL;
/* Related view for the next tab_create() call.
*
* When non-NULL, tab_create() creates the webview with
* webkit_web_view_new_with_related_view() so it shares the parent's
* WebProcess and window features (avoids the WindowFeatures assertion
* crash). Used by tab_manager_new_window(). */
static WebKitWebView *g_target_related_view = NULL;
/* Dynamic array of tab_info_t pointers, indexed by notebook page number. */
static tab_info_t **g_tabs = NULL;
static int g_tab_count = 0;
@@ -76,6 +95,9 @@ static int g_tab_cap = 0;
static tab_info_t *tab_create(const char *url);
static GtkWidget *build_tab_label(tab_info_t *tab);
static GtkWidget *tab_find_notebook(GtkWidget *page);
static int tab_array_add(tab_info_t *tab);
static int tab_array_find(tab_info_t *tab);
static GtkWidget *build_hamburger_menu(tab_info_t *tab);
static char *normalize_url(const char *input);
static void on_tab_close_clicked_proxy_new(GtkMenuItem *item, gpointer data);
@@ -490,7 +512,10 @@ static gboolean on_completion_match_selected(GtkEntryCompletion *completion,
static void on_tab_close_clicked(GtkButton *btn, gpointer user_data) {
tab_info_t *tab = (tab_info_t *)user_data;
(void)btn;
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), tab->page);
/* Look up the tab's index in the global g_tabs array by pointer.
* This works for tabs in any window's notebook (the index returned
* by gtk_notebook_page_num only matches g_tabs for the main window). */
int index = tab_array_find(tab);
if (index >= 0) {
tab_manager_close_tab(index);
}
@@ -501,7 +526,8 @@ static gboolean on_tab_label_button_press(GtkWidget *widget,
gpointer user_data) {
(void)widget;
tab_info_t *tab = (tab_info_t *)user_data;
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), tab->page);
/* Look up the tab's index in the global g_tabs array by pointer. */
int index = tab_array_find(tab);
if (index < 0) return FALSE;
@@ -761,79 +787,60 @@ static GtkWidget *tab_manager_new_window(const char *url,
gtk_window_set_title(GTK_WINDOW(window), "sovereign browser");
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
/* Create a notebook for this window (so future tabs could be added;
* for now it holds a single page). */
/* Create a notebook for this window. It holds the full tab
* infrastructure (toolbar, URL entry, tab label, favicon, etc.)
* built by tab_create(), so the new window looks and behaves like
* the main window. */
GtkWidget *notebook = gtk_notebook_new();
gtk_notebook_set_scrollable(GTK_NOTEBOOK(notebook), FALSE);
gtk_notebook_set_show_border(GTK_NOTEBOOK(notebook), FALSE);
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(notebook), TRUE);
gtk_container_add(GTK_CONTAINER(window), notebook);
/* Create the webview. Prefer webkit_web_view_new_with_related_view
* so the new webview shares the parent's WebProcess and window
* features (avoids the WindowFeatures assertion crash). Fall back
* to the shared context if no related view is provided. */
WebKitWebView *wv = NULL;
if (related_view != NULL && WEBKIT_IS_WEB_VIEW(related_view)) {
wv = WEBKIT_WEB_VIEW(webkit_web_view_new_with_related_view(related_view));
} else {
wv = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(g_ctx));
}
if (wv == NULL) {
/* Build a full tab via tab_create(). Set g_target_notebook so the
* tab is added to this window's notebook (not the main notebook),
* and g_target_related_view so the webview is created with
* webkit_web_view_new_with_related_view() (shares the parent's
* WebProcess and window features, avoiding the WindowFeatures
* assertion crash). Restore both globals afterwards. */
g_target_notebook = notebook;
g_target_related_view = (related_view != NULL &&
WEBKIT_IS_WEB_VIEW(related_view))
? related_view : NULL;
tab_info_t *tab = tab_create(url);
g_target_related_view = NULL;
g_target_notebook = NULL;
if (tab == NULL) {
gtk_widget_destroy(window);
return NULL;
}
/* Apply the same WebKitSettings as regular tabs. */
WebKitSettings *settings = webkit_web_view_get_settings(wv);
webkit_settings_set_enable_developer_extras(settings, TRUE);
webkit_settings_set_enable_javascript(settings, TRUE);
webkit_settings_set_javascript_can_open_windows_automatically(settings, TRUE);
webkit_settings_set_allow_file_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_universal_access_from_file_urls(settings, TRUE);
webkit_settings_set_allow_modal_dialogs(settings, TRUE);
/* Register the tab in the global g_tabs array and add it to the
* new window's notebook. tab_create() already loaded the URL and
* wired all per-tab signals (load-changed, decide-policy, create,
* context-menu, favicon, url-entry, etc.) with the tab_info_t as
* user_data, so the new window's tab gets the same behavior as a
* main-window tab. */
int index = tab_array_add(tab);
if (index < 0) {
g_free(tab);
gtk_widget_destroy(window);
return NULL;
}
/* Inject window.nostr into this webview. */
nostr_inject_setup(wv);
const browser_settings_t *s = settings_get();
int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(notebook),
tab->page, tab->tab_label);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook), tab->page,
s->tab_drag_reorder);
/* Connect the key-press handler (browser shortcuts). */
g_signal_connect(G_OBJECT(wv), "key-press-event",
G_CALLBACK(on_key_press), NULL);
/* Ensure the webview expands to fill the window. */
gtk_widget_set_vexpand(GTK_WIDGET(wv), TRUE);
gtk_widget_set_hexpand(GTK_WIDGET(wv), TRUE);
/* Build a minimal tab label for the notebook page. */
GtkWidget *label_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
GtkWidget *title_lbl = gtk_label_new("Loading…");
gtk_label_set_ellipsize(GTK_LABEL(title_lbl), PANGO_ELLIPSIZE_END);
gtk_label_set_max_width_chars(GTK_LABEL(title_lbl), 20);
gtk_widget_set_hexpand(title_lbl, TRUE);
gtk_box_pack_start(GTK_BOX(label_box), title_lbl, TRUE, TRUE, 0);
/* Add the webview as a notebook page. */
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), GTK_WIDGET(wv),
label_box);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(notebook), GTK_WIDGET(wv),
TRUE);
/* Wire the same webview signals as tab_create(). We pass NULL as
* user_data for handlers that expect a tab_info_t* — those handlers
* only dereference user_data for UI updates (URL bar, tab title,
* favicon) that don't apply to a standalone window webview. The
* navigation-relevant handlers (decide-policy, create, load-failed)
* don't need the tab pointer. */
g_signal_connect(wv, "load-changed",
G_CALLBACK(on_load_changed), NULL);
g_signal_connect(wv, "load-failed",
G_CALLBACK(on_load_failed), NULL);
g_signal_connect(wv, "decide-policy",
G_CALLBACK(on_decide_policy), NULL);
g_signal_connect(wv, "create",
G_CALLBACK(on_create_webview), NULL);
g_signal_connect(wv, "context-menu",
G_CALLBACK(on_webview_context_menu), NULL);
/* Show the tab widgets before switching to it. */
gtk_widget_show_all(tab->page);
gtk_widget_show_all(tab->tab_label);
gtk_notebook_set_current_page(GTK_NOTEBOOK(notebook), page_num);
/* Window lifecycle: focus-in updates the active window/notebook
* pointers; destroy reverts to the main window but does NOT quit. */
@@ -842,11 +849,6 @@ static GtkWidget *tab_manager_new_window(const char *url,
g_signal_connect(window, "destroy",
G_CALLBACK(on_aux_window_destroy), NULL);
/* Load the URL. */
if (url && url[0]) {
webkit_web_view_load_uri(wv, url);
}
/* Show everything and present the window. */
gtk_widget_show_all(window);
gtk_window_present(GTK_WINDOW(window));
@@ -855,9 +857,9 @@ static GtkWidget *tab_manager_new_window(const char *url,
g_active_window = GTK_WINDOW(window);
g_active_notebook = notebook;
g_print("[windows] Created new window %p for %s\n",
(void *)window, url ? url : "(none)");
return GTK_WIDGET(wv);
g_print("[windows] Created new window %p (tab %d) for %s\n",
(void *)window, index, url ? url : "(none)");
return GTK_WIDGET(tab->webview);
}
static GtkWidget *build_tab_label(tab_info_t *tab) {
@@ -1049,8 +1051,7 @@ static void on_load_changed(WebKitWebView *webview,
char *slash = strchr(host_buf, '/');
if (slash) *slash = '\0';
if (host_buf[0]) {
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook),
tab->page);
int index = tab_array_find(tab);
if (index >= 0) {
tab_manager_set_title(index, host_buf);
}
@@ -1063,10 +1064,9 @@ static void on_load_changed(WebKitWebView *webview,
uri ? uri : "(null)",
(title && title[0]) ? title : "(none)");
/* Update tab title (only for tabs in the main notebook). */
/* Update tab title (works for tabs in any window's notebook). */
if (tab != NULL && title && title[0]) {
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook),
tab->page);
int index = tab_array_find(tab);
if (index >= 0) {
tab_manager_set_title(index, title);
}
@@ -1745,8 +1745,59 @@ static void tab_array_remove(int index) {
g_tab_count--;
}
/* Find the index of a tab in the global g_tabs array by pointer.
*
* The g_tabs array is a flat list of all open tabs across all windows.
* Notebook page numbers only match g_tabs indices for the main window;
* auxiliary windows have their own page numbering. Internal handlers
* (close button, tab label clicks, load-changed title updates) should
* use this to look up the g_tabs index rather than gtk_notebook_page_num,
* which returns the page number within a single notebook. */
static int tab_array_find(tab_info_t *tab) {
if (tab == NULL) return -1;
for (int i = 0; i < g_tab_count; i++) {
if (g_tabs[i] == tab) return i;
}
return -1;
}
/* ── Tab creation ─────────────────────────────────────────────────── */
/* Return the notebook that the next new tab should be added to.
*
* Priority:
* 1. g_target_notebook — explicitly set by tab_manager_new_window()
* to place the first tab into the new window's notebook.
* 2. g_active_notebook — the currently focused window's notebook, so
* Ctrl+T / "New Tab" opens in the active window.
* 3. g_notebook — the main window's notebook (fallback). */
static GtkWidget *get_effective_notebook(void) {
if (g_target_notebook != NULL) return g_target_notebook;
if (g_active_notebook != NULL) return g_active_notebook;
return g_notebook;
}
/* Find the GtkNotebook that contains the given tab page widget.
*
* Tabs can live in either the main window's notebook (g_notebook) or an
* auxiliary window's notebook (g_active_notebook when it differs). This
* checks the active notebook first, then the main notebook, and returns
* whichever one actually contains the page (gtk_notebook_page_num >= 0).
*
* Returns NULL if the page is not found in either notebook. */
static GtkWidget *tab_find_notebook(GtkWidget *page) {
if (page == NULL) return NULL;
if (g_active_notebook != NULL && g_active_notebook != g_notebook) {
if (gtk_notebook_page_num(GTK_NOTEBOOK(g_active_notebook), page) >= 0)
return g_active_notebook;
}
if (g_notebook != NULL) {
if (gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook), page) >= 0)
return g_notebook;
}
return NULL;
}
static tab_info_t *tab_create(const char *url) {
const browser_settings_t *s = settings_get();
@@ -1767,8 +1818,19 @@ static tab_info_t *tab_create(const char *url) {
snprintf(tab->current_url, sizeof(tab->current_url), "%s", default_url);
snprintf(tab->title, sizeof(tab->title), "Loading…");
/* Create the webview from the shared context. */
tab->webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(g_ctx));
/* Create the webview. Prefer a related view (set by
* tab_manager_new_window) so the new webview shares the parent's
* WebProcess and window features — this avoids the
* std::optional<WindowFeatures> assertion crash that occurs with
* webkit_web_view_new_with_context() for target="_blank" windows.
* Fall back to the shared context for normal new tabs. */
if (g_target_related_view != NULL &&
WEBKIT_IS_WEB_VIEW(g_target_related_view)) {
tab->webview = WEBKIT_WEB_VIEW(
webkit_web_view_new_with_related_view(g_target_related_view));
} else {
tab->webview = WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(g_ctx));
}
/* Enable developer extras + security settings (same as original main.c). */
WebKitSettings *settings = webkit_web_view_get_settings(tab->webview);
@@ -2257,11 +2319,14 @@ int tab_manager_new_tab(const char *url) {
return -1;
}
/* Add to notebook. */
int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(g_notebook),
/* Add to the effective notebook (active window's notebook, or the
* main notebook as fallback). This makes Ctrl+T / "New Tab" open in
* the focused window instead of always the main window. */
GtkWidget *nb = get_effective_notebook();
int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(nb),
tab->page, tab->tab_label);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(g_notebook), tab->page,
s->tab_drag_reorder);
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(nb), tab->page,
s->tab_drag_reorder);
/* Show the tab widgets before switching to it — the page must be
* realized/mapped before it can become the current page and receive
@@ -2270,7 +2335,7 @@ int tab_manager_new_tab(const char *url) {
gtk_widget_show_all(tab->tab_label);
/* Switch to the new tab (now that it's visible). */
gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), page_num);
gtk_notebook_set_current_page(GTK_NOTEBOOK(nb), page_num);
/* Give the URL entry keyboard focus so the user can immediately
* type a URL. Select all text so typing replaces the current URL.
@@ -2289,15 +2354,33 @@ void tab_manager_close_tab(int index) {
tab_info_t *tab = g_tabs[index];
if (tab == NULL) return;
/* Find the notebook this tab belongs to — it may be the main
* window's notebook or an auxiliary window's notebook. */
GtkWidget *nb = tab_find_notebook(tab->page);
if (nb == NULL) nb = g_notebook; /* fallback */
/* Remove from notebook (this destroys the page widget). */
gtk_notebook_remove_page(GTK_NOTEBOOK(g_notebook), index);
gtk_notebook_remove_page(GTK_NOTEBOOK(nb), index);
/* Remove from our array. */
tab_array_remove(index);
g_print("[tabs] Closed tab %d, remaining: %d\n", index, g_tab_count);
/* If no tabs left, quit the app. */
/* If this was an auxiliary window's last tab, close that window.
* The destroy handler (on_aux_window_destroy) reverts the active
* window pointer to the main window. We detect "aux window" by
* checking that the notebook we removed from is not g_notebook and
* now has zero pages. */
if (nb != g_notebook &&
gtk_notebook_get_n_pages(GTK_NOTEBOOK(nb)) == 0) {
GtkWidget *toplevel = gtk_widget_get_toplevel(nb);
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
gtk_window_close(GTK_WINDOW(toplevel));
}
}
/* If the main window has no tabs left, quit the app. */
if (g_tab_count == 0) {
g_print("[tabs] Last tab closed, exiting.\n");
if (g_window) {
@@ -2307,15 +2390,26 @@ void tab_manager_close_tab(int index) {
}
void tab_manager_close_active(void) {
int index = tab_manager_get_active_index();
/* Resolve the active tab by widget pointer (works across windows),
* then look up its g_tabs index. Using tab_manager_get_active_index()
* directly would return the active notebook's page number, which only
* matches the g_tabs index for the main window. */
tab_info_t *tab = tab_manager_get_active();
int index = tab_array_find(tab);
if (index >= 0) {
tab_manager_close_tab(index);
}
}
int tab_manager_get_active_index(void) {
if (g_notebook == NULL) return -1;
return gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook));
/* Use the active window's notebook (defaults to g_notebook via
* get_effective_notebook). The returned page number is only a valid
* g_tabs index for the main window; callers that need the tab_info_t
* should use tab_manager_get_active(), which resolves by widget
* pointer to support auxiliary windows. */
GtkWidget *nb = get_effective_notebook();
if (nb == NULL) return -1;
return gtk_notebook_get_current_page(GTK_NOTEBOOK(nb));
}
void tab_manager_switch_to(int index) {
@@ -2325,9 +2419,21 @@ void tab_manager_switch_to(int index) {
}
tab_info_t *tab_manager_get_active(void) {
int index = tab_manager_get_active_index();
if (index < 0 || index >= g_tab_count) return NULL;
return g_tabs[index];
/* Resolve the active tab by looking at the active notebook's current
* page widget and finding the matching tab_info_t in g_tabs by
* pointer. This works across multiple windows: the notebook page
* number only matches the g_tabs index for the main window, so we
* can't rely on it for auxiliary windows. */
GtkWidget *nb = get_effective_notebook();
if (nb == NULL) return NULL;
gint page = gtk_notebook_get_current_page(GTK_NOTEBOOK(nb));
if (page < 0) return NULL;
GtkWidget *page_widget = gtk_notebook_get_nth_page(GTK_NOTEBOOK(nb), page);
if (page_widget == NULL) return NULL;
for (int i = 0; i < g_tab_count; i++) {
if (g_tabs[i] && g_tabs[i]->page == page_widget) return g_tabs[i];
}
return NULL;
}
tab_info_t *tab_manager_get(int index) {
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.18"
#define SB_VERSION "v0.0.21"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 18
#define SB_VERSION_PATCH 21
#endif /* SOVEREIGN_BROWSER_VERSION_H */