250 lines
9.6 KiB
Markdown
250 lines
9.6 KiB
Markdown
# 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
|
||
|
||
```mermaid
|
||
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
|
||
|
||
1. **Per-tab toolbar.** Each notebook page is a vertical `GtkBox` containing
|
||
the toolbar (hamburger + URL entry) on top and the `WebKitWebView` below.
|
||
This is simpler than a shared toolbar that has to swap signal handlers on
|
||
tab switch, and it matches the user's preference.
|
||
|
||
2. **Shared `WebKitWebContext`.** All `WebKitWebView` instances are created
|
||
from the same context. The `sovereign://` 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.
|
||
|
||
3. **Per-webview `nostr_inject_setup()`.** The `window.nostr` injection uses
|
||
`WebKitUserContentManager`, which is per-webview. Each new tab calls
|
||
`nostr_inject_setup()` on its own webview.
|
||
|
||
4. **`tab_manager.c` owns the `GtkNotebook`.** 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.
|
||
|
||
5. **Settings module.** A new `settings.c`/`settings.h` persists user
|
||
preferences to `~/.sovereign_browser/settings.conf` (simple key=value
|
||
text file). Configurable options:
|
||
|
||
| Setting | Default | Description |
|
||
|---------|---------|-------------|
|
||
| `restore_session` | `true` | Restore open tabs on next launch |
|
||
| `new_tab_url` | `https://example.com` | URL loaded in new tabs |
|
||
| `tab_bar_position` | `top` | `top` / `bottom` / `left` / `right` |
|
||
| `show_tab_close_buttons` | `true` | Show per-tab close button |
|
||
| `middle_click_close` | `true` | Middle-click on tab closes it |
|
||
| `ctrl_tab_switch` | `true` | Ctrl+Tab cycles tabs |
|
||
| `max_tabs` | `50` | Maximum simultaneous tabs |
|
||
| `tab_drag_reorder` | `true` | Allow drag to reorder tabs |
|
||
|
||
6. **Session module.** `session.c`/`session.h` saves the list of open tab URLs
|
||
(one per line) to `~/.sovereign_browser/session.txt` on window destroy, and
|
||
restores them on startup if `settings.restore_session` is true.
|
||
|
||
## New files
|
||
|
||
### `src/settings.h` / `src/settings.c`
|
||
|
||
```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`
|
||
|
||
```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`
|
||
|
||
```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` / `toolbar` creation.
|
||
- Create `WebKitWebContext` once, 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
|
||
via `tab_manager_get_active()` instead of receiving it as a parameter.
|
||
- `on_load_changed` and `on_load_failed` are connected per-tab inside
|
||
`tab_manager_new_tab()` and update the per-tab URL entry and tab title.
|
||
- `on_url_activate` loads the URL into the active tab's webview.
|
||
- Keyboard shortcuts: a `key-press-event` handler on the main window checks
|
||
for Ctrl+T / Ctrl+W / Ctrl+Tab / Ctrl+Shift+Tab / Ctrl+PageUp / Ctrl+PageDown.
|
||
- `on_window_destroy` calls `session_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: `GtkLabel` with ellipsize end, max width ~150px
|
||
- Close button: `GtkButton` with `window-close-symbolic` icon, only shown if
|
||
`settings.show_tab_close_buttons` is 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
|
||
|
||
1. **settings.c/h** — foundation, no UI yet, just load/save + global singleton
|
||
2. **tab_manager.c/h** — core notebook management, per-tab page creation,
|
||
tab label widget, new/close/switch
|
||
3. **session.c/h** — save/restore using tab_manager API
|
||
4. **main.c refactor** — wire up tab_manager, move context setup, rewire
|
||
callbacks to use active tab
|
||
5. **Tab interactions** — middle-click close, right-click menu, drag reorder
|
||
6. **Keyboard shortcuts** — window-level key-press handler
|
||
7. **Session save/restore** — integrate into startup and destroy
|
||
8. **Settings dialog** — GTK dialog wired to settings module
|
||
9. **Makefile + docs** — build and documentation updates
|