diff --git a/Makefile b/Makefile index 4a8ba80..499e63c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm BIN := sovereign_browser -SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/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 := 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 $(BIN): $(SRC) $(NOSTR_LIB) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS) diff --git a/VERSION b/VERSION index 9789c4c..ceddfb2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.14 +0.0.15 diff --git a/plans/keyboard-shortcuts.md b/plans/keyboard-shortcuts.md new file mode 100644 index 0000000..36a310f --- /dev/null +++ b/plans/keyboard-shortcuts.md @@ -0,0 +1,342 @@ +# Plan: Configurable Keyboard Shortcuts + +## Overview + +Make browser-level keyboard shortcuts user-configurable on the `sovereign://settings` page. The user presses the desired key combination directly in the page (no typing "ctrl-F1" into a text box); the page captures the physical key press, formats it, and persists it. On the C side, the existing hardcoded `on_key_press()` dispatcher in [`main.c`](src/main.c:230) is refactored to look up bindings from a new `shortcuts` module. + +## Nostr sync (NIP-78, kind 30078) + +Per [NIP-78](https://github.com/nostr-protocol/nips/blob/master/78.md), kind `30078` is an addressable event for **arbitrary custom app data** — its first listed use case is literally *"User personal settings on Nostr clients."* We use it to sync all sovereign_browser user settings (not just shortcuts) across devices. + +### Event design + +``` +{ + "kind": 30078, + "pubkey": "", + "tags": [ + ["d", "sovereign_browser"], + ["client", "sovereign_browser"] + ], + "content": "", + "created_at": , + ... +} +``` + +- **`d` tag**: `"sovereign_browser"` — namespacing so other NIP-78 apps don't collide. +- **`content`**: NIP-44 encrypted (to self, same pattern as bookmarks in [`bookmarks.c`](src/bookmarks.c:162)) JSON object containing all user-configurable settings: shortcuts bindings, tab preferences, agent server config, bootstrap relays, security toggles. This keeps potentially sensitive config (relay list, origins) private. +- **Plaintext payload** (before encryption): + ```json + { + "shortcuts": { + "new_tab": "t", + "close_tab": "w", + ... + }, + "settings": { + "restore_session": true, + "new_tab_url": "https://example.com", + "tab_bar_position": "top", + ... + } + } + ``` + +### Sync flow + +```mermaid +flowchart TD + A["User changes shortcut in settings UI"] --> B["shortcuts_set action accel"] + B --> C["Update in-memory g_parsed"] + B --> D["db_kv_set shortcut.X"] + D --> E["Debounced publish 500ms"] + E --> F["Serialize all settings to JSON"] + F --> G["NIP-44 encrypt to self"] + G --> H["Build kind 30078 event d=sovereign_browser"] + H --> I["Sign with signer"] + I --> J["Publish to bootstrap relays"] + I --> K["Store in SQLite db_store_event"] + + L["Browser startup / login"] --> M["relay_fetch_thread fetches kind 30078"] + M --> N["Store in SQLite"] + N --> O["shortcuts_load_from_nostr: decrypt + parse"] + O --> P["Merge: Nostr values override local defaults, local overrides win on conflict timestamp"] + P --> Q["Populate in-memory g_parsed + write db_kv cache"] +``` + +### Merge / conflict policy + +On startup, after the relay fetch thread completes (existing [`relay_fetch_thread`](src/relay_fetch.c) already fetches kinds 0/3/10002 — we add 30078), we decrypt the latest kind 30078 `d=sovereign_browser` event and merge: + +1. **Local SQLite `db_kv` is the fast-path cache** — `shortcuts_load()` reads from it for instant startup (no waiting for relays). +2. **When the Nostr event arrives**, compare its `created_at` to a stored `settings.nostr_synced_at` timestamp in `db_kv`. +3. **If Nostr is newer**: overwrite local `db_kv` from the decrypted payload, reload in-memory bindings, reload the settings page if open. +4. **If local is newer** (user changed something offline): keep local, and schedule a re-publish so Nostr catches up. +5. **First run / no Nostr event**: defaults are used; nothing published until the user changes something. + +This mirrors how bookmarks work but is simpler (single event vs. one-per-directory). + +### Read-only / no-login mode + +When there's no signer (read-only or `--no-login` mode), NIP-44 encryption isn't available. In that case: +- Settings still work fully via local `db_kv` (the fast path). +- No kind 30078 event is published or fetched. +- The settings page shows a note: *"Nostr sync disabled — log in with a signing key to sync settings across devices."* + +### What goes in the Nostr payload + +| Setting | Synced via NIP-78? | Rationale | +|---------|-------------------|-----------| +| Shortcut bindings | ✅ Yes | User preference, device-independent | +| Tab bar position, close buttons, drag reorder | ✅ Yes | UI preference | +| `new_tab_url`, `max_tabs` | ✅ Yes | Preference | +| `restore_session` | ❌ No | Device-specific (which tabs were open) | +| `agent_server_enabled`, `agent_server_port`, `agent_allowed_origins` | ❌ No | Device-specific (port may differ, origins may be localhost) | +| `agent_login_timeout_ms` | ❌ No | Device-specific | +| `bootstrap_relays` | ✅ Yes | User's relay list should follow them | +| Security toggles (dev_extras, file_access, universal_access) | ❌ No | Device-specific debugging flags | + +The `settings_sync.c` module (see below) knows which keys belong in the Nostr payload via a whitelist. + +## WebKitGTK built-in shortcuts (research finding) + +WebKitGTK webviews consume standard **editing** shortcuts at the webview level *before* they reach our window-level `key-press-event` handler: + +- `Ctrl+C` / `Ctrl+V` / `Ctrl+X` / `Ctrl+A` — clipboard +- `Ctrl+Z` / `Ctrl+Y` — undo/redo +- `Ctrl+F` — in-page find (only if a `WebKitFindController` is wired up; currently not) +- `Tab` / `Shift+Tab` — focus traversal within the page +- `F5` / `Ctrl+R` — reload (WebKit handles these for the webview) + +**Browser-level** shortcuts (tab management, URL focus, tab cycling) are **not** handled by WebKit — they are ours, currently hardcoded in [`on_key_press()`](src/main.c:230). These are the ones we make configurable. + +**Conflict policy**: If a user assigns a binding that WebKit consumes for the focused webview (e.g. `Ctrl+C`), the webview eats it and our handler never sees it. The settings UI will warn when a chosen combo is in a "reserved by web content" list, but will still allow it (the user may want it to apply only when the URL bar / tab strip has focus). Recommended defaults stick to `Ctrl+` letter combos and `F1`–`F12` which are safe. + +## Binding format + +Use **GTK accelerator string format** (parseable by `gtk_accelerator_parse`): + +- `t` — Ctrl+T +- `Tab` — Ctrl+Shift+Tab +- `F5` — Ctrl+F5 +- `F1` — bare function key + +This gives us free parsing/serialization via `gtk_accelerator_parse()` / `gtk_accelerator_name()` and keeps the stored representation compact and human-readable. + +## Action registry + +A fixed enum of browser actions the user can bind. Each has: id, display label, description, default binding. + +| Action ID | Label | Default binding | +|----------------------|------------------------|---------------------------| +| `new_tab` | New tab | `t` | +| `close_tab` | Close tab | `w` | +| `focus_url` | Focus URL bar | `l` | +| `next_tab` | Next tab | `Tab` | +| `prev_tab` | Previous tab | `Tab` | +| `next_tab_pagedown` | Next tab (PageDown) | `Page_Down` | +| `prev_tab_pageup` | Previous tab (PageUp) | `Page_Up` | +| `reload` | Reload page | `F5` | +| `force_reload` | Force reload | `F5` | +| `go_back` | Go back | `Left` | +| `go_forward` | Go forward | `Right` | +| `find` | Find in page | `f` | +| `open_settings` | Open settings page | `comma` | +| `new_identity` | Switch identity | `i` | +| `toggle_fullscreen` | Toggle fullscreen | `F11` | + +(Exact list is adjustable; the registry is a single table in `shortcuts.c` so adding/removing actions is trivial.) + +## Storage + +Each binding is one row in the existing `key_value` table: + +- key: `shortcut.` (e.g. `shortcut.new_tab`) +- value: accelerator string (e.g. `t`) + +Loaded once at startup into an in-memory array indexed by action enum. `settings_save()` is not extended — the shortcuts module owns its own load/save via `db_kv_get`/`db_kv_set`. + +## New module: `shortcuts.h` / `shortcuts.c` + +```mermaid +flowchart LR + A["db_kv_get shortcut.*"] --> B["shortcuts_load"] + B --> C["g_bindings array"] + D["on_key_press event"] --> E["shortcuts_lookup event"] + C --> E + E --> F["action enum or -1"] + F --> G["dispatch switch in main.c"] + H["sovereign://settings/set?key=shortcut.X"] --> I["shortcuts_set action accel"] + I --> J["db_kv_set + update g_bindings"] +``` + +### `shortcuts.h` (public API) + +```c +typedef enum { + SHORTCUT_NEW_TAB = 0, + SHORTCUT_CLOSE_TAB, + SHORTCUT_FOCUS_URL, + SHORTCUT_NEXT_TAB, + SHORTCUT_PREV_TAB, + SHORTCUT_NEXT_TAB_PAGEDOWN, + SHORTCUT_PREV_TAB_PAGEUP, + SHORTCUT_RELOAD, + SHORTCUT_FORCE_RELOAD, + SHORTCUT_GO_BACK, + SHORTCUT_GO_FORWARD, + SHORTCUT_FIND, + SHORTCUT_OPEN_SETTINGS, + SHORTCUT_NEW_IDENTITY, + SHORTCUT_TOGGLE_FULLSCREEN, + SHORTCUT_COUNT +} shortcut_action_t; + +/* Load all bindings from db_kv_get into the in-memory array. + * Call once at startup after db_init(). Missing keys use defaults. */ +void shortcuts_load(void); + +/* Look up which action a key event matches, or -1 if none. + * Parses the stored accelerator strings once (cached) and compares + * keyval + mods. */ +int shortcuts_lookup(GdkEventKey *event); + +/* Get the accelerator string for an action (e.g. for the settings UI). */ +const char *shortcuts_get(shortcut_action_t action); + +/* Set and persist a new binding for an action. + * accel_str must be parseable by gtk_accelerator_parse. + * Returns 0 on success, -1 on parse failure. */ +int shortcuts_set(shortcut_action_t action, const char *accel_str); + +/* Reset an action to its default binding (and persist). */ +void shortcuts_reset(shortcut_action_t action); + +/* Reset all actions to defaults (and persist). */ +void shortcuts_reset_all(void); + +/* Metadata for the settings UI: label, description, default accel. */ +typedef struct { + const char *id; /* "new_tab" */ + const char *label; /* "New tab" */ + const char *desc; /* "Open a new tab" */ + const char *dflt; /* "t" */ +} shortcut_meta_t; +const shortcut_meta_t *shortcuts_meta(shortcut_action_t action); +``` + +### `shortcuts.c` (implementation notes) + +- Static array `static struct { guint keyval; GdkModifierType mods; } g_parsed[SHORTCUT_COUNT];` populated by `gtk_accelerator_parse()` on load and on each `shortcuts_set()`. +- `shortcuts_lookup()` applies `gtk_accelerator_get_default_mod_mask()` to the event state, then linear-scans `g_parsed` comparing `keyval` and masked `mods`. Returns first match (actions are distinct enough that collisions are the user's problem; the UI warns on duplicates). +- `shortcuts_set()` calls `gtk_accelerator_parse(accel_str, &key, &mods)`; returns -1 if parse fails. On success, updates `g_parsed`, writes `db_kv_set("shortcut.", accel_str)`. + +## Refactor `on_key_press()` in `main.c` + +Replace the hardcoded if/else chain with: + +```c +static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data) { + int action = shortcuts_lookup(event); + if (action < 0) return FALSE; + + switch ((shortcut_action_t)action) { + case SHORTCUT_NEW_TAB: tab_manager_new_tab(NULL); return TRUE; + case SHORTCUT_CLOSE_TAB: tab_manager_close_active(); return TRUE; + case SHORTCUT_FOCUS_URL: /* ... existing Ctrl+L logic ... */ return TRUE; + case SHORTCUT_NEXT_TAB: tab_manager_next(); return TRUE; + case SHORTCUT_PREV_TAB: tab_manager_prev(); return TRUE; + /* ... etc ... */ + case SHORTCUT_FIND: /* open find bar (new) */ return TRUE; + case SHORTCUT_OPEN_SETTINGS: on_menu_settings(NULL, NULL); return TRUE; + /* ... */ + default: return FALSE; + } +} +``` + +The `ctrl_tab_switch` boolean setting is removed from the settings struct (or kept as a quick on/off for the whole shortcuts feature). Decision: **keep `ctrl_tab_switch` as a master "enable keyboard shortcuts" toggle** — when off, `shortcuts_lookup()` returns -1 immediately. This preserves the existing one-click disable behavior. + +## Settings page UI — "Keyboard Shortcuts" section + +Added to [`handle_settings_page()`](src/nostr_bridge.c:393) HTML, between "Tabs" and "Agent Server". For each action in the registry, render a row: + +``` +[ New tab ] [ t ] [ Change ] [ Reset ] +[ Close tab ] [ w ] [ Change ] [ Reset ] +... +[ Reset all to defaults ] +``` + +### Key-capture interaction (the "press the keys" UX) + +Each row's **Change** button focuses a hidden capture `` and shows "Press keys…". The input has a `keydown` listener that: + +1. Calls `event.preventDefault()` and `event.stopPropagation()` so the key combo is never delivered to the page. +2. Reads `event.ctrlKey`, `event.altKey`, `event.shiftKey`, `event.metaKey` and `event.key` / `event.code`. +3. Maps the JS key to a GTK accelerator string. Mapping rules: + - Modifier prefix: `` / `` / `` / `` in that order. + - Key normalization: letters → lowercase; `F1`–`F12` as-is; special keys (`Tab`, `Page_Down`, `Page_Up`, `Left`, `Right`, `comma`, `space`) mapped to GDK key names. + - Bare modifier-only presses are ignored (wait for a real key). +4. Displays the formatted string in the row (e.g. `Ctrl+Shift+T` for display, but sends `t` to the backend). +5. On `Enter` or blur, commits via `fetch('sovereign://settings/set?key=shortcut.new_tab&value=' + encodeURIComponent(accel))`. +6. `Escape` cancels capture without committing. + +A small JS lookup table maps JS `event.code`/`event.key` → GDK key name for the ~20 keys we care about. This keeps the capture logic in the page without needing a round-trip to C for each keypress. + +### Backend handling in `handle_settings_set()` + +Add a branch in the key/value path of [`handle_settings_set()`](src/nostr_bridge.c:705): + +```c +if (strncmp(key, "shortcut.", 9) == 0) { + const char *action_id = key + 9; + /* find action by id string in the registry */ + shortcut_action_t a = shortcuts_action_from_id(action_id); + if (a < 0) { respond_error_json(...); return; } + if (shortcuts_set(a, value) != 0) { + respond_error_json(request, -1, "Invalid key combination"); + return; + } + /* respond with ok + reload */ +} +``` + +Also add a `shortcut.reset` and `shortcut.reset_all` pseudo-keys (or a separate `sovereign://settings/shortcut-reset?action=X` endpoint) for the Reset buttons. + +### Conflict / duplicate warnings + +The settings page JS, after a successful commit, can fetch all current bindings (embedded in the page HTML on render) and highlight any other action that now shares the same accelerator string. The backend `shortcuts_set()` does **not** reject duplicates — last-wins at lookup time — but the UI flags them in red so the user notices. + +## Files to create / modify + +| File | Change | +|------|--------| +| `src/shortcuts.h` | **New** — action enum + public API | +| `src/shortcuts.c` | **New** — registry, load/save (from `db_kv`), lookup, parse | +| `src/settings_sync.h` | **New** — NIP-78 sync API: `settings_sync_init()`, `settings_sync_publish()`, `settings_sync_merge_from_nostr()` | +| `src/settings_sync.c` | **New** — serialize whitelisted settings + shortcuts to JSON, NIP-44 encrypt to self, build/sign kind 30078 `d=sovereign_browser`, publish to relays, store in SQLite; decrypt + merge on fetch. Reuses the pattern from [`bookmarks.c`](src/bookmarks.c:214) `publish_directory()`. | +| `src/main.c` | Refactor [`on_key_press()`](src/main.c:230) to dispatch via `shortcuts_lookup()`; call `shortcuts_load()` after `settings_load()`; call `settings_sync_init()` after login; add action handlers (find, settings, fullscreen, back/forward) | +| `src/nostr_bridge.c` | Add "Keyboard Shortcuts" section to [`handle_settings_page()`](src/nostr_bridge.c:393) HTML + key-capture JS; add `shortcut.*` branch to [`handle_settings_set()`](src/nostr_bridge.c:705); trigger debounced `settings_sync_publish()` on any settings change | +| `src/settings.h` / `settings.c` | Repurpose `ctrl_tab_switch` → `shortcuts_enabled` master toggle (or keep name + add alias); update defaults + load/save | +| `src/relay_fetch.c` / `relay_fetch.h` | Add kind 30078 to the fetched kinds; after fetch, call `settings_sync_merge_from_nostr()` to decrypt and merge | +| `Makefile` | Add `src/shortcuts.c` and `src/settings_sync.c` to `SRC` | +| `src/tab_manager.h` / `tab_manager.c` | Add `tab_manager_find()` / find-bar helpers if `SHORTCUT_FIND` is implemented in this phase (can be stubbed for later) | + +## Phasing + +**Phase 1a (local):** shortcuts module + refactor `on_key_press` + settings UI with key capture. Covers all currently-hardcoded actions plus `reload`, `go_back`, `go_forward`, `open_settings`, `toggle_fullscreen` (these already have tab_manager/menu functions to call). Persistence via `db_kv` only. + +**Phase 1b (Nostr sync):** `settings_sync` module — publish kind 30078 on debounced settings changes, fetch + merge on startup. This makes shortcuts (and other whitelisted settings) sync across devices. + +**Phase 2 (future):** `find` (requires wiring `WebKitFindController` + a find bar widget), `new_identity` (calls `app_menu_switch_identity_proxy`), `force_reload` (bypass cache). + +## Testing + +- Manual: open `sovereign://settings`, press Change on "New tab", press `Ctrl+Q`, verify it persists and `Ctrl+Q` now opens a new tab on next key press (same session — `shortcuts_set` updates the in-memory array immediately). +- Restart browser, verify binding persisted in SQLite. +- Reset single + reset all buttons restore defaults. +- Duplicate-binding warning appears when two actions share a combo. +- Bare modifier press (just Ctrl) doesn't commit; Escape cancels. +- `shortcuts_enabled = false` disables all lookup. diff --git a/plans/search-engine.md b/plans/search-engine.md new file mode 100644 index 0000000..6881ea4 --- /dev/null +++ b/plans/search-engine.md @@ -0,0 +1,127 @@ +# Search Engine Integration & URL Bar Dropdown + +## Goal + +Integrate a search engine (DuckDuckGo default, user-changeable) into the URL bar. When the user types, a dropdown appears with **direct links first** (from history, bookmarks, and domain heuristics), followed by **search engine suggestions**. Selecting a direct link navigates to that URL; selecting a search suggestion goes to the search engine results page. + +## Architecture + +```mermaid +flowchart TD + A[User types in URL entry] --> B[on_url_changed callback] + B --> C[Clear suggestion list store] + C --> D[db_history_search query] + C --> E[bookmarks_search query] + C --> F[domain heuristic] + D --> G[Add direct-link rows to model] + E --> G + F --> G + B --> H[Async SoupRequest to DDG ac API] + H --> I[Add search-suggestion rows to model] + G --> J[GtkEntryCompletion dropdown shows] + I --> J + J --> K{User selects a row} + K -->|direct link| L[webkit_web_view_load_uri url] + K -->|search suggestion| M[load search engine URL with query] + K -->|Enter, no selection| N{Is input a URL?} + N -->|Yes| L + N -->|No| M +``` + +## Data Sources for Direct Links + +1. **History** — SQLite `history` table, searched by URL/title substring, ranked by `visit_count DESC, visited_at DESC`. These are sites the user has actually visited. +2. **Bookmarks** — In-memory bookmark list, searched by URL/title substring. +3. **Domain heuristic** — If the typed text has no spaces and no dots, offer `https://.org` and `https://.com` as "Navigate directly" options. Handles first-time visits to known domains. + +## Search Engine Suggestions + +DuckDuckGo autocomplete API: `https://duckduckgo.com/ac/?q=&type=list` +Returns a JSON array: `["wikipedia", "wikipedia english", ...]` +Fetched asynchronously via libsoup (already linked). Results populate the lower section of the dropdown. Selecting one navigates to `https://duckduckgo.com/?q=`. + +## Search Engine Configuration + +Built-in engines hardcoded in `search.c`: + +| Engine | Search URL | Suggestion URL | +|--------|-----------|----------------| +| DuckDuckGo | `https://duckduckgo.com/?q=%s` | `https://duckduckgo.com/ac/?q=%s&type=list` | +| Google | `https://www.google.com/search?q=%s` | `https://suggestqueries.google.com/complete/search?client=firefox&q=%s` | +| Brave | `https://search.brave.com/search?q=%s` | `https://search.brave.com/api/suggest?q=%s` | +| Startpage | `https://www.startpage.com/sp/search?query=%s` | (none — no autocomplete) | +| Searx | `https://searx.be/search?q=%s` | (none) | + +The selected engine name is stored in settings (`search_engine` key) and synced via NIP-78. + +## Files to Create + +### `src/search.h` + `src/search.c` +- `search_engine_t` struct: name, search_url_template, suggestion_url_template +- `search_engines_get()` — returns array of built-in engines +- `search_engine_get_active()` — returns the currently selected engine +- `search_engine_build_search_url(const char *query)` — builds a search URL from the active engine's template +- `search_engine_build_suggestion_url(const char *query)` — builds the suggestion API URL +- `search_suggest_fetch_async(const char *query, GFunc callback, gpointer user_data)` — async libsoup HTTP GET, parses JSON array, calls callback with results +- `search_is_url(const char *input)` — heuristic: returns TRUE if input looks like a URL (has scheme, or has a dot with no spaces) + +## Files to Modify + +### `src/settings.h` + `src/settings.c` +- Add `char search_engine[64]` to `browser_settings_t` (default "duckduckgo") +- Load/save the `search_engine` key from db_kv + +### `src/db.h` + `src/db.c` +- Add `db_history_search(const char *query, char ***urls_out, char ***titles_out, int *count_out, int limit)` — `WHERE url LIKE '%q%' OR title LIKE '%q%' ORDER BY visit_count DESC, visited_at DESC LIMIT ?` +- Add index on `history(url)` and `history(title)` for search performance + +### `src/tab_manager.c` +- Add a `GtkListStore` (per-tab or shared) with columns: display text, URL, item type (direct/suggestion), icon name +- Add `GtkEntryCompletion` to each tab's `url_entry` with a custom match function +- Add `on_url_changed` callback — rebuilds the list store on each keystroke: + 1. Clear store + 2. Query history via `db_history_search()` + 3. Query bookmarks via `bookmarks_get_dirs()` + filter + 4. Add domain heuristic entries if applicable + 5. Fire async `search_suggest_fetch_async()` — on completion, append suggestion rows +- Override `match-selected` signal — read the URL column; if it's a direct link, navigate; if it's a search suggestion, build search URL and navigate +- Modify `on_url_activate` — if input is not a URL (per `search_is_url()`), build a search URL and navigate instead of prepending `https://` +- Modify `normalize_url` — if input is not a URL and not an about: URL, treat as a search query + +### `src/nostr_bridge.c` +- Add a "Search Engine" section to the settings page HTML with a `