Files
sovereign_browser/src/settings.c
T

521 lines
23 KiB
C

/*
* settings.c — browser preferences for sovereign_browser
*
* Persists settings to the SQLite database (key_value table). Each setting
* is stored as a key-value pair. The database must be initialized (db_init())
* before settings_load() is called.
*/
#include "settings.h"
#include "db.h"
#include <gtk/gtk.h>
#include <stdio.h>
#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 ─────────────────────────────────────────────── */
static browser_settings_t g_settings;
/* ── Defaults ─────────────────────────────────────────────────────── */
static void settings_set_defaults(browser_settings_t *s) {
s->restore_session = FALSE;
snprintf(s->new_tab_url, sizeof(s->new_tab_url), "%s",
SETTINGS_NEW_TAB_URL_DEFAULT);
s->tab_bar_position = GTK_POS_TOP;
s->show_tab_close_buttons = TRUE;
s->middle_click_close = TRUE;
s->ctrl_tab_switch = TRUE;
s->max_tabs = SETTINGS_MAX_TABS_DEFAULT;
s->tab_drag_reorder = TRUE;
s->agent_server_enabled = TRUE;
s->json_viewer_enabled = TRUE;
s->agent_server_port = SETTINGS_AGENT_PORT_DEFAULT;
snprintf(s->agent_allowed_origins, sizeof(s->agent_allowed_origins), "*");
s->agent_login_timeout_ms = SETTINGS_AGENT_LOGIN_TIMEOUT_DEFAULT;
snprintf(s->bootstrap_relays, sizeof(s->bootstrap_relays), "%s",
SETTINGS_BOOTSTRAP_RELAYS_DEFAULT);
snprintf(s->search_engine, sizeof(s->search_engine), "%s",
SETTINGS_SEARCH_ENGINE_DEFAULT);
snprintf(s->nostr_helper_apps, sizeof(s->nostr_helper_apps), "%s",
SETTINGS_NOSTR_HELPER_APPS_DEFAULT);
s->theme_dark = FALSE; /* default: light mode */
s->inspector_x = -1; /* -1 = let window manager decide */
s->inspector_y = -1;
s->inspector_w = -1;
s->inspector_h = -1;
/* Agent LLM provider settings */
snprintf(s->agent_llm_base_url, sizeof(s->agent_llm_base_url), "%s",
SETTINGS_AGENT_LLM_BASE_URL_DEFAULT);
s->agent_llm_api_key[0] = '\0'; /* empty by default */
snprintf(s->agent_llm_model, sizeof(s->agent_llm_model), "%s",
SETTINGS_AGENT_LLM_MODEL_DEFAULT);
s->agent_llm_system_prompt[0] = '\0'; /* legacy alias for skill_template */
s->agent_max_iterations = SETTINGS_AGENT_MAX_ITERATIONS_DEFAULT;
/* Sovereign Browser Skill defaults. The template defaults to the
* built-in system prompt; the other fields describe the skill. */
snprintf(s->agent_skill_name,
sizeof(s->agent_skill_name), "%s",
SETTINGS_AGENT_SKILL_NAME_DEFAULT);
snprintf(s->agent_skill_description,
sizeof(s->agent_skill_description), "%s",
SETTINGS_AGENT_SKILL_DESCRIPTION_DEFAULT);
snprintf(s->agent_skill_template,
sizeof(s->agent_skill_template), "%s",
SETTINGS_AGENT_SYSTEM_PROMPT_DEFAULT);
snprintf(s->agent_skill_requires_tools,
sizeof(s->agent_skill_requires_tools), "%s",
SETTINGS_AGENT_SKILL_REQUIRES_TOOLS_DEFAULT);
/* Multi-provider catalog — empty by default. Populated from the
* d:user-settings Nostr event (global.agent.providers) on login.
* If no providers are loaded, a single default provider is seeded
* from the legacy agent_llm_base_url / agent_llm_api_key fields. */
s->agent_provider_count = 0;
s->agent_active_provider = -1;
s->agent_active_provider_name[0] = '\0';
memset(s->agent_providers, 0, sizeof(s->agent_providers));
s->tor_enabled = TRUE;
snprintf(s->tor_mode, sizeof(s->tor_mode), "auto");
snprintf(s->tor_binary_path, sizeof(s->tor_binary_path), "tor");
s->tor_attach_socks[0] = '\0';
s->tor_attach_control[0] = '\0';
snprintf(s->tor_data_dir, sizeof(s->tor_data_dir),
"~/.sovereign_browser/tor");
s->fips_enabled = TRUE;
snprintf(s->fips_mode, sizeof(s->fips_mode), "auto");
snprintf(s->fips_binary_path, sizeof(s->fips_binary_path), "fips");
s->fips_control_socket[0] = '\0';
snprintf(s->fips_config_dir, sizeof(s->fips_config_dir),
"~/.sovereign_browser/fips");
}
/* ── Parsing helpers ──────────────────────────────────────────────── */
static gboolean parse_bool(const char *value, gboolean fallback) {
if (value == NULL) return fallback;
if (strcasecmp(value, "true") == 0 ||
strcasecmp(value, "yes") == 0 ||
strcasecmp(value, "1") == 0 ||
strcasecmp(value, "on") == 0) {
return TRUE;
}
if (strcasecmp(value, "false") == 0 ||
strcasecmp(value, "no") == 0 ||
strcasecmp(value, "0") == 0 ||
strcasecmp(value, "off") == 0) {
return FALSE;
}
return fallback;
}
static int parse_int(const char *value, int fallback) {
if (value == NULL || value[0] == '\0') return fallback;
char *end = NULL;
long v = strtol(value, &end, 10);
if (end == value) return fallback;
return (int)v;
}
static int parse_position(const char *value, int fallback) {
if (value == NULL) return fallback;
if (strcasecmp(value, "top") == 0) return GTK_POS_TOP;
if (strcasecmp(value, "bottom") == 0) return GTK_POS_BOTTOM;
if (strcasecmp(value, "left") == 0) return GTK_POS_LEFT;
if (strcasecmp(value, "right") == 0) return GTK_POS_RIGHT;
/* Allow numeric GTK_POS_* values too. */
return parse_int(value, fallback);
}
/* ── Position to string ───────────────────────────────────────────── */
static const char *position_to_string(int pos) {
switch (pos) {
case GTK_POS_TOP: return "top";
case GTK_POS_BOTTOM: return "bottom";
case GTK_POS_LEFT: return "left";
case GTK_POS_RIGHT: return "right";
default: return "top";
}
}
/* ── 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_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;
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("json_viewer_enabled");
if (val) g_settings.json_viewer_enabled = parse_bool(val, g_settings.json_viewer_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");
if (val) g_settings.restore_session = parse_bool(val, g_settings.restore_session);
val = db_kv_get("new_tab_url");
if (val) snprintf(g_settings.new_tab_url, sizeof(g_settings.new_tab_url), "%s", val);
val = db_kv_get("tab_bar_position");
if (val) g_settings.tab_bar_position = parse_position(val, g_settings.tab_bar_position);
val = db_kv_get("show_tab_close_buttons");
if (val) g_settings.show_tab_close_buttons = parse_bool(val, g_settings.show_tab_close_buttons);
val = db_kv_get("middle_click_close");
if (val) g_settings.middle_click_close = parse_bool(val, g_settings.middle_click_close);
val = db_kv_get("ctrl_tab_switch");
if (val) g_settings.ctrl_tab_switch = parse_bool(val, g_settings.ctrl_tab_switch);
val = db_kv_get("max_tabs");
if (val) {
g_settings.max_tabs = parse_int(val, g_settings.max_tabs);
if (g_settings.max_tabs < 1) g_settings.max_tabs = 1;
}
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("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("nostr_helper_apps");
if (val) snprintf(g_settings.nostr_helper_apps,
sizeof(g_settings.nostr_helper_apps), "%s", val);
val = db_kv_get("tor.enabled");
if (val) g_settings.tor_enabled = parse_bool(val, g_settings.tor_enabled);
val = db_kv_get("tor.mode");
if (val) snprintf(g_settings.tor_mode, sizeof(g_settings.tor_mode), "%s", val);
val = db_kv_get("tor.binary_path");
if (val) snprintf(g_settings.tor_binary_path, sizeof(g_settings.tor_binary_path), "%s", val);
val = db_kv_get("tor.attach_socks");
if (val) snprintf(g_settings.tor_attach_socks, sizeof(g_settings.tor_attach_socks), "%s", val);
val = db_kv_get("tor.attach_control");
if (val) snprintf(g_settings.tor_attach_control, sizeof(g_settings.tor_attach_control), "%s", val);
val = db_kv_get("tor.data_dir");
if (val) snprintf(g_settings.tor_data_dir, sizeof(g_settings.tor_data_dir), "%s", val);
val = db_kv_get("fips.enabled");
if (val) g_settings.fips_enabled = parse_bool(val, g_settings.fips_enabled);
val = db_kv_get("fips.mode");
if (val) snprintf(g_settings.fips_mode, sizeof(g_settings.fips_mode), "%s", val);
val = db_kv_get("fips.binary_path");
if (val) snprintf(g_settings.fips_binary_path, sizeof(g_settings.fips_binary_path), "%s", val);
val = db_kv_get("fips.control_socket");
if (val) snprintf(g_settings.fips_control_socket, sizeof(g_settings.fips_control_socket), "%s", val);
val = db_kv_get("fips.config_dir");
if (val) snprintf(g_settings.fips_config_dir, sizeof(g_settings.fips_config_dir), "%s", val);
/* Agent LLM provider settings — these are the "resolved" values
* (active provider's base_url + api_key + selected model). They are
* populated from db_kv here for local persistence, and overwritten
* by settings_sync_merge_from_nostr() from global.agent.providers +
* sovereign_browser.agent when a Nostr event is available. */
val = db_kv_get("agent.llm_base_url");
if (val) snprintf(g_settings.agent_llm_base_url, sizeof(g_settings.agent_llm_base_url), "%s", val);
val = db_kv_get("agent.llm_api_key");
if (val) snprintf(g_settings.agent_llm_api_key, sizeof(g_settings.agent_llm_api_key), "%s", val);
val = db_kv_get("agent.llm_model");
if (val) snprintf(g_settings.agent_llm_model, sizeof(g_settings.agent_llm_model), "%s", val);
val = db_kv_get("agent.llm_system_prompt");
if (val) snprintf(g_settings.agent_llm_system_prompt, sizeof(g_settings.agent_llm_system_prompt), "%s", val);
val = db_kv_get("agent.max_iterations");
if (val) g_settings.agent_max_iterations = atoi(val);
/* Sovereign Browser Skill fields. The template mirrors
* agent_llm_system_prompt for backward compatibility — if the
* legacy key is set but the new skill_template key is not, the
* template falls back to the legacy value (or the default). */
val = db_kv_get("agent.skill_name");
if (val) snprintf(g_settings.agent_skill_name,
sizeof(g_settings.agent_skill_name), "%s", val);
val = db_kv_get("agent.skill_description");
if (val) snprintf(g_settings.agent_skill_description,
sizeof(g_settings.agent_skill_description), "%s", val);
val = db_kv_get("agent.skill_template");
if (val && val[0]) {
snprintf(g_settings.agent_skill_template,
sizeof(g_settings.agent_skill_template), "%s", val);
} else if (g_settings.agent_llm_system_prompt[0]) {
/* Migrate from the legacy system_prompt field. */
snprintf(g_settings.agent_skill_template,
sizeof(g_settings.agent_skill_template), "%s",
g_settings.agent_llm_system_prompt);
}
/* Keep the legacy alias in sync (truncates to 4096 safely). */
if (g_settings.agent_skill_template[0]) {
g_strlcpy(g_settings.agent_llm_system_prompt,
g_settings.agent_skill_template,
sizeof(g_settings.agent_llm_system_prompt));
}
val = db_kv_get("agent.skill_requires_tools");
if (val) snprintf(g_settings.agent_skill_requires_tools,
sizeof(g_settings.agent_skill_requires_tools), "%s", val);
/* If no provider catalog was loaded from Nostr (agent_provider_count
* == 0), seed a single "default" provider from the resolved
* agent_llm_base_url / agent_llm_api_key so the agents config page
* has something to show. */
if (g_settings.agent_provider_count == 0) {
agent_provider_t *p = &g_settings.agent_providers[0];
memset(p, 0, sizeof(*p));
snprintf(p->name, sizeof(p->name), "default");
snprintf(p->base_url, sizeof(p->base_url), "%s",
g_settings.agent_llm_base_url);
snprintf(p->api_key, sizeof(p->api_key), "%s",
g_settings.agent_llm_api_key);
if (g_settings.agent_llm_model[0]) {
snprintf(p->models[0], sizeof(p->models[0]), "%s",
g_settings.agent_llm_model);
p->model_count = 1;
}
g_settings.agent_provider_count = 1;
g_settings.agent_active_provider = 0;
snprintf(g_settings.agent_active_provider_name,
sizeof(g_settings.agent_active_provider_name), "default");
}
}
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");
db_kv_set_to_file(gpath, "json_viewer_enabled",
g_settings.json_viewer_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");
db_kv_set("new_tab_url", g_settings.new_tab_url);
db_kv_set("tab_bar_position", position_to_string(g_settings.tab_bar_position));
db_kv_set("show_tab_close_buttons", g_settings.show_tab_close_buttons ? "true" : "false");
db_kv_set("middle_click_close", g_settings.middle_click_close ? "true" : "false");
db_kv_set("ctrl_tab_switch", g_settings.ctrl_tab_switch ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.max_tabs);
db_kv_set("max_tabs", buf);
db_kv_set("tab_drag_reorder", g_settings.tab_drag_reorder ? "true" : "false");
/* 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("nostr_helper_apps", g_settings.nostr_helper_apps);
db_kv_set("tor.enabled", g_settings.tor_enabled ? "true" : "false");
db_kv_set("tor.mode", g_settings.tor_mode);
db_kv_set("tor.binary_path", g_settings.tor_binary_path);
db_kv_set("tor.attach_socks", g_settings.tor_attach_socks);
db_kv_set("tor.attach_control", g_settings.tor_attach_control);
db_kv_set("tor.data_dir", g_settings.tor_data_dir);
db_kv_set("fips.enabled", g_settings.fips_enabled ? "true" : "false");
db_kv_set("fips.mode", g_settings.fips_mode);
db_kv_set("fips.binary_path", g_settings.fips_binary_path);
db_kv_set("fips.control_socket", g_settings.fips_control_socket);
db_kv_set("fips.config_dir", g_settings.fips_config_dir);
/* Agent LLM provider settings — resolved values (active provider). */
db_kv_set("agent.llm_base_url", g_settings.agent_llm_base_url);
db_kv_set("agent.llm_api_key", g_settings.agent_llm_api_key);
db_kv_set("agent.llm_model", g_settings.agent_llm_model);
db_kv_set("agent.llm_system_prompt", g_settings.agent_skill_template);
db_kv_set("agent.provider", g_settings.agent_active_provider_name);
snprintf(buf, sizeof(buf), "%d", g_settings.agent_max_iterations);
db_kv_set("agent.max_iterations", buf);
/* Sovereign Browser Skill fields. */
db_kv_set("agent.skill_name", g_settings.agent_skill_name);
db_kv_set("agent.skill_description", g_settings.agent_skill_description);
db_kv_set("agent.skill_template", g_settings.agent_skill_template);
db_kv_set("agent.skill_requires_tools",
g_settings.agent_skill_requires_tools);
}
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) {
return &g_settings;
}
browser_settings_t *settings_get_mutable(void) {
return &g_settings;
}