9.6 KiB
Browser Tabs Implementation Plan
Overview
Add full browser tab support to sovereign_browser. Each tab gets its own
toolbar (URL bar + hamburger menu) and WebKitWebView. All tabs share a single
WebKitWebContext, so the sovereign:// Nostr bridge, security settings, and
TLS policy apply automatically to every tab. Tab behavior is configurable via
a new settings module.
Architecture
flowchart TB
subgraph Window
Notebook[GtkNotebook — tab strip]
NewTabBtn[New Tab Button +]
end
subgraph TabPage
Toolbar[Toolbar: hamburger + URL entry]
Webview[WebKitWebView]
end
Notebook -->|page 0| TabPage
Notebook -->|page 1| TabPage
Notebook -->|page N| TabPage
TabPage -->|shared| Ctx[WebKitWebContext — sovereign bridge, security, TLS]
Ctx --> NostrBridge[nostr_bridge.c]
Ctx --> NostrInject[nostr_inject.c — per-webview]
TabManager[tab_manager.c] -->|manages| Notebook
Settings[settings.c] -->|configures| TabManager
Session[session.c] -->|save/restore| TabManager
Key design decisions
-
Per-tab toolbar. Each notebook page is a vertical
GtkBoxcontaining the toolbar (hamburger + URL entry) on top and theWebKitWebViewbelow. This is simpler than a shared toolbar that has to swap signal handlers on tab switch, and it matches the user's preference. -
Shared
WebKitWebContext. AllWebKitWebViewinstances are created from the same context. Thesovereign://URI scheme registration, security manager flags, and TLS error policy are set once on the context and apply to all tabs. No per-tab re-registration needed. -
Per-webview
nostr_inject_setup(). Thewindow.nostrinjection usesWebKitUserContentManager, which is per-webview. Each new tab callsnostr_inject_setup()on its own webview. -
tab_manager.cowns theGtkNotebook. It provides functions to create, close, switch, and query tabs. Each tab is tracked in a struct holding the webview, URL entry, hamburger button, and tab label widget. -
Settings module. A new
settings.c/settings.hpersists user preferences to~/.sovereign_browser/settings.conf(simple key=value text file). Configurable options:Setting Default Description restore_sessiontrueRestore open tabs on next launch new_tab_urlhttps://example.comURL loaded in new tabs tab_bar_positiontoptop/bottom/left/rightshow_tab_close_buttonstrueShow per-tab close button middle_click_closetrueMiddle-click on tab closes it ctrl_tab_switchtrueCtrl+Tab cycles tabs max_tabs50Maximum simultaneous tabs tab_drag_reordertrueAllow drag to reorder tabs -
Session module.
session.c/session.hsaves the list of open tab URLs (one per line) to~/.sovereign_browser/session.txton window destroy, and restores them on startup ifsettings.restore_sessionis true.
New files
src/settings.h / src/settings.c
typedef struct {
gboolean restore_session;
char new_tab_url[512];
int tab_bar_position; /* GTK_POS_TOP, etc. */
gboolean show_tab_close_buttons;
gboolean middle_click_close;
gboolean ctrl_tab_switch;
int max_tabs;
gboolean tab_drag_reorder;
} browser_settings_t;
void settings_load(browser_settings_t *out);
void settings_save(const browser_settings_t *s);
const browser_settings_t *settings_get(void); /* global singleton */
src/tab_manager.h / src/tab_manager.c
typedef struct {
WebKitWebView *webview;
GtkWidget *url_entry;
GtkWidget *hamburger;
GtkWidget *tab_label; /* composite: icon + title + close btn */
char current_url[2048];
char title[256];
} tab_info_t;
void tab_manager_init(GtkContainer *parent, WebKitWebContext *ctx);
int tab_manager_new_tab(const char *url);
void tab_manager_close_tab(int index);
void tab_manager_close_active(void);
int tab_manager_get_active_index(void);
tab_info_t *tab_manager_get_active(void);
tab_info_t *tab_manager_get(int index);
int tab_manager_count(void);
void tab_manager_set_title(int index, const char *title);
src/session.h / src/session.c
void session_save(void); /* called on window destroy */
int session_restore(void); /* called at startup, returns tab count created */
Modified files
src/main.c — major refactor
- Remove the single
webview/url_entry/toolbarcreation. - Create
WebKitWebContextonce, configure security +sovereign://bridge on it (existing code moves here, unchanged logic). - Call
tab_manager_init()with the window's vbox and the shared context. - The hamburger menu builder moves into
tab_manager.c(or a shared helper) since it now needs to reference the per-tab webview, not a global. - All menu callbacks (
on_menu_reload,on_menu_stop,on_menu_inspector,on_menu_open_file,on_menu_history_item) fetch the active tab's webview viatab_manager_get_active()instead of receiving it as a parameter. on_load_changedandon_load_failedare connected per-tab insidetab_manager_new_tab()and update the per-tab URL entry and tab title.on_url_activateloads the URL into the active tab's webview.- Keyboard shortcuts: a
key-press-eventhandler on the main window checks for Ctrl+T / Ctrl+W / Ctrl+Tab / Ctrl+Shift+Tab / Ctrl+PageUp / Ctrl+PageDown. on_window_destroycallssession_save()before cleanup.
src/nostr_inject.c — no changes
nostr_inject_setup() already takes a WebKitWebView* and is called
per-webview. The tab manager calls it for each new tab.
src/nostr_bridge.c — no changes
The bridge is registered on the shared WebKitWebContext. All webviews on
that context automatically get the sovereign:// scheme handler.
src/history.c — no changes
History is already global. All tabs contribute to and read from the same history list.
Makefile
Add src/settings.c src/tab_manager.c src/session.c to SRC.
Tab UI layout
┌──────────────────────────────────────────────────────┐
│ [Tab 1 ×] [Tab 2 ×] [Tab 3 ×] [+] │ ← GtkNotebook tab strip
├──────────────────────────────────────────────────────┤
│ [☰] https://example.com____________ │ ← per-tab toolbar
├──────────────────────────────────────────────────────┤
│ │
│ WebKitWebView │ ← per-tab webview
│ │
└──────────────────────────────────────────────────────┘
Tab label widget (composite)
Each tab label is a horizontal GtkBox:
[favicon] [Title text…] [×]
- Favicon: placeholder icon for now (can be wired to WebKit favicon later)
- Title:
GtkLabelwith ellipsize end, max width ~150px - Close button:
GtkButtonwithwindow-close-symbolicicon, only shown ifsettings.show_tab_close_buttonsis true
Right-click context menu on tab label
- New Tab
- Close Tab
- Close Other Tabs
- Close Tabs to the Right
- Duplicate Tab
- Reload Tab
Drag reordering
GtkNotebook supports drag reordering natively via
gtk_notebook_set_tab_reorderable(notebook, child, TRUE). Enabled when
settings.tab_drag_reorder is true.
Keyboard shortcuts
| Shortcut | Action |
|---|---|
| Ctrl+T | New tab |
| Ctrl+W | Close active tab |
| Ctrl+Tab | Next tab |
| Ctrl+Shift+Tab | Previous tab |
| Ctrl+PageUp | Previous tab |
| Ctrl+PageDown | Next tab |
| Ctrl+L | Focus URL bar of active tab |
Session restore
On startup, if settings.restore_session is true and
~/.sovereign_browser/session.txt exists and is non-empty, each line is
loaded as a tab URL. If the file is missing or empty, a single tab is opened
with the start URL (from command-line arg or settings.new_tab_url).
On window destroy, before gtk_main_quit(), the list of open tab URLs is
written to ~/.sovereign_browser/session.txt.
Settings UI
A new menu item "Settings…" in the hamburger menu opens a native GTK dialog
(not a web page, to keep it simple and avoid bootstrapping a
sovereign://settings renderer). The dialog has checkboxes and a text entry
for each configurable option. On "OK", settings_save() is called and
applicable changes (like tab bar position) are applied live.
Implementation order
- settings.c/h — foundation, no UI yet, just load/save + global singleton
- tab_manager.c/h — core notebook management, per-tab page creation, tab label widget, new/close/switch
- session.c/h — save/restore using tab_manager API
- main.c refactor — wire up tab_manager, move context setup, rewire callbacks to use active tab
- Tab interactions — middle-click close, right-click menu, drag reorder
- Keyboard shortcuts — window-level key-press handler
- Session save/restore — integrate into startup and destroy
- Settings dialog — GTK dialog wired to settings module
- Makefile + docs — build and documentation updates