Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31195c4fe2 | ||
|
|
a4b4c52a91 | ||
|
|
02a92f7a26 |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 ☀☾</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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user