v0.0.4 - Implemented browser tabs with GtkNotebook, per-tab toolbars, session restore, settings dialog, and keyboard shortcuts. Made Other Qube (qrexec) the default n_signer transport.
This commit is contained in:
2
Makefile
2
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 := 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
|
||||
|
||||
$(BIN): $(SRC) $(NOSTR_LIB)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS)
|
||||
|
||||
55
README.md
55
README.md
@@ -41,8 +41,9 @@ because trust moves to the layer where it belongs: your keys.
|
||||
|
||||
## Current status
|
||||
|
||||
Working proof-of-concept: a C99 + WebKitGTK window with a URL bar that loads
|
||||
real HTTPS pages. Verified loading `https://laantungir.net` cleanly. See
|
||||
Working browser: a C99 + WebKitGTK window with multi-tab support, a URL bar
|
||||
per tab, Nostr login, and `window.nostr` injection. Verified loading
|
||||
`https://laantungir.net` cleanly. See
|
||||
[`docs/webkit-poc-findings.md`](docs/webkit-poc-findings.md) for the friction
|
||||
report from the POC phase (and the Servo fallback exploration).
|
||||
|
||||
@@ -125,15 +126,53 @@ Supported methods:
|
||||
See [`plans/nostr-login-integration.md`](plans/nostr-login-integration.md) for
|
||||
the full implementation plan.
|
||||
|
||||
### Browser tabs
|
||||
|
||||
The browser supports multiple tabs via a `GtkNotebook`. Each tab has its own
|
||||
toolbar (hamburger menu + URL entry) and `WebKitWebView`. All tabs share a
|
||||
single `WebKitWebContext`, so the `sovereign://` Nostr bridge, security
|
||||
settings, and TLS policy apply to every tab automatically.
|
||||
|
||||
**Tab features:**
|
||||
|
||||
- New tab: `Ctrl+T` or the `+` button at the end of the tab strip
|
||||
- Close tab: `Ctrl+W`, per-tab close button, or middle-click on the tab label
|
||||
- Switch tabs: `Ctrl+Tab` / `Ctrl+Shift+Tab`, `Ctrl+PageUp` / `Ctrl+PageDown`
|
||||
- Focus URL bar: `Ctrl+L`
|
||||
- Right-click tab context menu: New Tab, Close Tab, Close Other Tabs, Close
|
||||
Tabs to the Right, Duplicate Tab, Reload Tab
|
||||
- Drag tabs to reorder
|
||||
- Tab titles update from page load
|
||||
- Session save/restore: open tabs are saved on exit and restored on next
|
||||
launch (configurable in Settings)
|
||||
|
||||
**Settings dialog** (hamburger menu → Settings…):
|
||||
|
||||
- Restore session on startup (default: on)
|
||||
- New tab URL (default: `https://example.com`)
|
||||
- Tab bar position: Top / Bottom / Left / Right (default: Top)
|
||||
- Show tab close buttons (default: on)
|
||||
- Middle-click to close tab (default: on)
|
||||
- Ctrl+Tab to switch tabs (default: on)
|
||||
- Maximum tabs (default: 50)
|
||||
- Allow drag to reorder tabs (default: on)
|
||||
|
||||
Settings are persisted to `~/.sovereign_browser/settings.conf`. Session state
|
||||
is saved to `~/.sovereign_browser/session.txt`.
|
||||
|
||||
See [`plans/browser-tabs.md`](plans/browser-tabs.md) for the full
|
||||
implementation plan.
|
||||
|
||||
## Roadmap
|
||||
|
||||
1. ✅ Base browser (WebKitGTK + C99, loads pages)
|
||||
2. ⏳ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t)
|
||||
3. ⏳ `window.nostr` injection (`sovereign://` URI scheme bridge)
|
||||
4. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
|
||||
5. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
|
||||
6. ⏳ `nostr://` content scheme
|
||||
7. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
|
||||
2. ✅ Browser tabs (GtkNotebook, per-tab toolbar, session restore, settings)
|
||||
3. ⏳ Nostr login (GTK dialog → nostr_core_lib → nostr_signer_t)
|
||||
4. ⏳ `window.nostr` injection (`sovereign://` URI scheme bridge)
|
||||
5. ⏳ Security strip (disable SOP/CORS, accept any cert, shared context)
|
||||
6. ⏳ FIPS URI scheme (`fips://` / `*.fips` via WebKitGTK scheme handler)
|
||||
7. ⏳ `nostr://` content scheme
|
||||
8. Future: agent runtime (didactyl hosting), multi-window, FIPS-distributed
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -275,8 +275,11 @@ flowchart LR
|
||||
|
||||
### Layers
|
||||
|
||||
1. **Host application (C99).** Owns the window, URL bar, tabs, key store, and
|
||||
the request router. This is where the "reckless" policy lives.
|
||||
1. **Host application (C99).** Owns the window, tab strip (`GtkNotebook`),
|
||||
per-tab toolbars (URL bar + hamburger menu), key store, and the request
|
||||
router. This is where the "reckless" policy lives. Tab management is in
|
||||
`tab_manager.c`, user preferences in `settings.c`, and session save/restore
|
||||
in `session.c`.
|
||||
|
||||
2. **Request router.** Inspects every outgoing URL:
|
||||
- `http://` / `https://` → engine's normal network stack (with CORS /
|
||||
|
||||
249
plans/browser-tabs.md
Normal file
249
plans/browser-tabs.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 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
|
||||
@@ -732,7 +732,7 @@ static void on_transport_changed(GtkComboBox *combo, gpointer user_data) {
|
||||
gtk_widget_hide(enum_btn);
|
||||
gtk_widget_hide(service_box);
|
||||
break;
|
||||
case 3: /* Qubes qrexec */
|
||||
case 3: /* Other Qube (Qubes qrexec) */
|
||||
gtk_label_set_text(GTK_LABEL(device_label), "Target Qube:");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nostr_signer");
|
||||
/* Set default value so the user doesn't need to type it. */
|
||||
@@ -954,21 +954,26 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(transport_combo), "USB Serial");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(transport_combo), "UNIX Socket");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(transport_combo), "TCP");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(transport_combo), "Qubes qrexec");
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(transport_combo), 0);
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(transport_combo), "Other Qube");
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(transport_combo), 3);
|
||||
gtk_box_pack_start(GTK_BOX(transport_box), transport_combo, FALSE, FALSE, 0);
|
||||
|
||||
/* Device path entry — label and placeholder adapt to transport type. */
|
||||
GtkWidget *device_label = gtk_label_new("Device Path:");
|
||||
/* Device path entry — label and placeholder adapt to transport type.
|
||||
* Default transport is "Other Qube" (qrexec), so initial UI reflects
|
||||
* that. The on_transport_changed callback updates these when the user
|
||||
* switches transport. */
|
||||
GtkWidget *device_label = gtk_label_new("Target Qube:");
|
||||
gtk_widget_set_halign(device_label, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(GTK_BOX(box), device_label, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *device_entry = gtk_entry_new();
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "/dev/ttyACM0");
|
||||
gtk_entry_set_text(GTK_ENTRY(device_entry), "nostr_signer");
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(device_entry), "nostr_signer");
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(device_entry), 40);
|
||||
gtk_box_pack_start(GTK_BOX(box), device_entry, FALSE, FALSE, 0);
|
||||
|
||||
/* Enumerate serial devices button (only shown for serial transport). */
|
||||
/* Enumerate serial devices button (only shown for serial transport).
|
||||
* Hidden by default since qrexec is the default transport. */
|
||||
GtkWidget *enum_btn = gtk_button_new_with_label("Detect Serial Devices");
|
||||
#if defined(NOSTR_ENABLE_NSIGNER_CLIENT)
|
||||
g_signal_connect(enum_btn, "clicked", G_CALLBACK(on_detect_serial), device_entry);
|
||||
@@ -976,6 +981,7 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_widget_set_sensitive(enum_btn, FALSE);
|
||||
#endif
|
||||
gtk_box_pack_start(GTK_BOX(box), enum_btn, FALSE, FALSE, 0);
|
||||
gtk_widget_hide(enum_btn);
|
||||
|
||||
/* Service name field (only shown for qrexec transport). */
|
||||
GtkWidget *service_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
@@ -989,8 +995,8 @@ static GtkWidget *create_nsigner_screen(login_ctx_t *ctx) {
|
||||
gtk_entry_set_width_chars(GTK_ENTRY(service_entry), 30);
|
||||
gtk_box_pack_start(GTK_BOX(service_box), service_entry, FALSE, FALSE, 0);
|
||||
|
||||
/* Initially hide the service field (serial is the default transport). */
|
||||
gtk_widget_hide(service_box);
|
||||
/* Show the service field by default (qrexec is the default transport). */
|
||||
gtk_widget_show_all(service_box);
|
||||
|
||||
/* Connect transport change callback to update the UI dynamically. */
|
||||
g_signal_connect(transport_combo, "changed",
|
||||
|
||||
744
src/main.c
744
src/main.c
@@ -1,15 +1,25 @@
|
||||
/*
|
||||
* sovereign_browser — WebKitGTK + C99
|
||||
*
|
||||
* Minimal browser: a GTK window with a URL entry, a hamburger menu, and a
|
||||
* WebKitWebView. Type a URL, press Enter, the page loads. The hamburger
|
||||
* menu to the left of the URL bar exposes roadmap actions (reload, security
|
||||
* strip toggle, FIPS, Nostr signing) as placeholders to wire up next.
|
||||
* Multi-tab browser: a GTK window with a GtkNotebook tab strip. Each tab
|
||||
* has its own toolbar (hamburger menu + URL entry) and WebKitWebView. All
|
||||
* webviews share a single WebKitWebContext so the sovereign:// Nostr bridge,
|
||||
* security settings, and TLS policy apply to every tab.
|
||||
*
|
||||
* On startup, a Nostr login dialog is shown. The user signs in with a local
|
||||
* key (nsec), seed phrase, read-only npub, NIP-46 remote signer, or n_signer
|
||||
* hardware. The resulting nostr_signer_t is held globally and will be used
|
||||
* to inject window.nostr into every page (Phase 2).
|
||||
* hardware. The resulting nostr_signer_t is held globally and backs the
|
||||
* window.nostr injection in every tab.
|
||||
*
|
||||
* Features:
|
||||
* - Tab creation (Ctrl+T, + button), closing (Ctrl+W, close button,
|
||||
* middle-click)
|
||||
* - Tab switching (Ctrl+Tab, Ctrl+Shift+Tab, Ctrl+PageUp/Down)
|
||||
* - Right-click tab context menu (New, Close, Close Others, Close to
|
||||
* Right, Duplicate, Reload)
|
||||
* - Tab drag reordering
|
||||
* - Session save/restore (configurable in Settings)
|
||||
* - Settings dialog (tab preferences, persisted to disk)
|
||||
*
|
||||
* Build: make. Run: ./sovereign_browser [url]
|
||||
*/
|
||||
@@ -27,11 +37,15 @@
|
||||
#include "nostr_bridge.h"
|
||||
#include "nostr_inject.h"
|
||||
#include "history.h"
|
||||
#include "settings.h"
|
||||
#include "tab_manager.h"
|
||||
#include "session.h"
|
||||
#include "nostr_core/nostr_core.h"
|
||||
|
||||
/* ---- Global state --------------------------------------------------- *
|
||||
* The signer is created at login and held for the lifetime of the session.
|
||||
* Phase 2 will use it to back the window.nostr injection.
|
||||
* It backs the window.nostr injection in every tab via the sovereign://
|
||||
* URI scheme bridge.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
@@ -42,57 +56,21 @@ typedef struct {
|
||||
} app_state_t;
|
||||
|
||||
static app_state_t g_state = {0};
|
||||
static GtkWindow *g_window = NULL;
|
||||
|
||||
/* ---- Menu action callbacks ------------------------------------------- */
|
||||
/* ---- Menu proxy functions ------------------------------------------- *
|
||||
* These wrap the app_state_t-aware callbacks so they match GTK signal
|
||||
* handler signatures and can be called from tab_manager.c's hamburger
|
||||
* menu builder.
|
||||
*/
|
||||
|
||||
static void on_menu_reload(GtkMenuItem *item, WebKitWebView *webview) {
|
||||
(void)item;
|
||||
if (webview != NULL) {
|
||||
webkit_web_view_reload_bypass_cache(webview);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_stop(GtkMenuItem *item, WebKitWebView *webview) {
|
||||
(void)item;
|
||||
if (webview != NULL) {
|
||||
webkit_web_view_stop_loading(webview);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_security_strip(GtkMenuItem *item, gpointer data) {
|
||||
(void)data;
|
||||
gboolean active = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item));
|
||||
g_print("[menu] security strip: %s\n", active ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
static void on_menu_fips(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
g_print("[menu] FIPS URI scheme: not yet implemented (roadmap)\n");
|
||||
}
|
||||
|
||||
static void on_menu_nostr_sign(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_state.signer) {
|
||||
g_print("[menu] Nostr signing: signer active, pubkey=%s\n",
|
||||
g_state.pubkey_hex);
|
||||
} else if (g_state.readonly) {
|
||||
g_print("[menu] Nostr signing: read-only mode (no signer)\n");
|
||||
} else {
|
||||
g_print("[menu] Nostr signing: no signer loaded\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Show the login dialog again (switch identity / re-auth). */
|
||||
static void on_menu_switch_identity(GtkMenuItem *item, gpointer data) {
|
||||
void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
GtkWindow *window = GTK_WINDOW(data);
|
||||
if (window == NULL) return;
|
||||
|
||||
login_result_t result;
|
||||
if (login_dialog_run(window, &result) == 0) {
|
||||
/* Free the old signer. */
|
||||
if (g_state.signer) {
|
||||
nostr_signer_free(g_state.signer);
|
||||
}
|
||||
@@ -102,7 +80,6 @@ static void on_menu_switch_identity(GtkMenuItem *item, gpointer data) {
|
||||
g_state.pubkey_hex[64] = '\0';
|
||||
g_state.readonly = (result.method == KEY_STORE_METHOD_READONLY);
|
||||
|
||||
/* Update the bridge so window.nostr uses the new signer. */
|
||||
nostr_bridge_set_signer(g_state.signer, g_state.pubkey_hex,
|
||||
g_state.readonly);
|
||||
|
||||
@@ -111,7 +88,19 @@ static void on_menu_switch_identity(GtkMenuItem *item, gpointer data) {
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_logout(GtkMenuItem *item, gpointer data) {
|
||||
void app_menu_lock_session_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_state.signer) {
|
||||
nostr_signer_free(g_state.signer);
|
||||
g_state.signer = NULL;
|
||||
}
|
||||
g_state.readonly = TRUE;
|
||||
nostr_bridge_set_signer(NULL, g_state.pubkey_hex, TRUE);
|
||||
g_print("[identity] session locked (signer cleared, identity preserved)\n");
|
||||
}
|
||||
|
||||
void app_menu_logout_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_state.signer) {
|
||||
@@ -125,12 +114,36 @@ static void on_menu_logout(GtkMenuItem *item, gpointer data) {
|
||||
g_print("[identity] logged out\n");
|
||||
}
|
||||
|
||||
static void on_menu_about(GtkMenuItem *item, gpointer data) {
|
||||
void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data) {
|
||||
(void)data;
|
||||
gboolean active = gtk_check_menu_item_get_active(item);
|
||||
g_print("[menu] security strip: %s\n", active ? "ON" : "OFF");
|
||||
}
|
||||
|
||||
void app_menu_fips_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
g_print("[menu] FIPS URI scheme: not yet implemented (roadmap)\n");
|
||||
}
|
||||
|
||||
void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_state.signer) {
|
||||
g_print("[menu] Nostr signing: signer active, pubkey=%s\n",
|
||||
g_state.pubkey_hex);
|
||||
} else if (g_state.readonly) {
|
||||
g_print("[menu] Nostr signing: read-only mode (no signer)\n");
|
||||
} else {
|
||||
g_print("[menu] Nostr signing: no signer loaded\n");
|
||||
}
|
||||
}
|
||||
|
||||
void app_menu_about_proxy(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
GtkWidget *window = GTK_WIDGET(data);
|
||||
if (window == NULL) {
|
||||
return;
|
||||
}
|
||||
if (window == NULL) return;
|
||||
|
||||
GtkWidget *dialog = gtk_message_dialog_new(
|
||||
GTK_WINDOW(window),
|
||||
GTK_DIALOG_DESTROY_WITH_PARENT,
|
||||
@@ -143,306 +156,240 @@ static void on_menu_about(GtkMenuItem *item, gpointer data) {
|
||||
gtk_widget_show_all(dialog);
|
||||
}
|
||||
|
||||
/* Toggle the WebKit Web Inspector. */
|
||||
static void on_menu_inspector(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(data);
|
||||
if (webview == NULL) return;
|
||||
/* ---- Settings dialog ------------------------------------------------ */
|
||||
|
||||
WebKitWebInspector *inspector = webkit_web_view_get_inspector(webview);
|
||||
if (inspector == NULL) return;
|
||||
static void on_settings_dialog_response(GtkDialog *dialog, gint response_id,
|
||||
gpointer user_data) {
|
||||
(void)user_data;
|
||||
if (response_id == GTK_RESPONSE_ACCEPT) {
|
||||
browser_settings_t *s = settings_get_mutable();
|
||||
|
||||
/* Just show the inspector — WebKitGTK handles the toggle. */
|
||||
webkit_web_inspector_show(inspector);
|
||||
}
|
||||
|
||||
/* History item callback: load the URL from the menu item label. */
|
||||
static void on_menu_history_item(GtkMenuItem *item, gpointer data) {
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(data);
|
||||
if (webview == NULL || item == NULL) return;
|
||||
const char *url = gtk_menu_item_get_label(item);
|
||||
if (url && url[0]) {
|
||||
webkit_web_view_load_uri(webview, url);
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear history callback. */
|
||||
static void on_menu_history_clear(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
history_clear();
|
||||
g_print("[history] cleared\n");
|
||||
}
|
||||
|
||||
/* Rebuild the history submenu when it's shown (so it's always current). */
|
||||
static void on_history_menu_show(GtkWidget *menu, gpointer user_data) {
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(user_data);
|
||||
|
||||
/* Remove all existing items. */
|
||||
GList *children = gtk_container_get_children(GTK_CONTAINER(menu));
|
||||
/* Read the widgets by name from the dialog's content area. */
|
||||
GtkWidget *content = gtk_dialog_get_content_area(dialog);
|
||||
GList *children = gtk_container_get_children(GTK_CONTAINER(content));
|
||||
for (GList *l = children; l != NULL; l = l->next) {
|
||||
gtk_widget_destroy(GTK_WIDGET(l->data));
|
||||
GtkWidget *child = GTK_WIDGET(l->data);
|
||||
const char *name = gtk_widget_get_name(child);
|
||||
if (name == NULL) continue;
|
||||
|
||||
if (strcmp(name, "restore_session") == 0) {
|
||||
s->restore_session = gtk_toggle_button_get_active(
|
||||
GTK_TOGGLE_BUTTON(child));
|
||||
} else if (strcmp(name, "new_tab_url") == 0) {
|
||||
const char *text = gtk_entry_get_text(GTK_ENTRY(child));
|
||||
snprintf(s->new_tab_url, sizeof(s->new_tab_url), "%s", text);
|
||||
} else if (strcmp(name, "show_tab_close_buttons") == 0) {
|
||||
s->show_tab_close_buttons = gtk_toggle_button_get_active(
|
||||
GTK_TOGGLE_BUTTON(child));
|
||||
} else if (strcmp(name, "middle_click_close") == 0) {
|
||||
s->middle_click_close = gtk_toggle_button_get_active(
|
||||
GTK_TOGGLE_BUTTON(child));
|
||||
} else if (strcmp(name, "ctrl_tab_switch") == 0) {
|
||||
s->ctrl_tab_switch = gtk_toggle_button_get_active(
|
||||
GTK_TOGGLE_BUTTON(child));
|
||||
} else if (strcmp(name, "tab_drag_reorder") == 0) {
|
||||
s->tab_drag_reorder = gtk_toggle_button_get_active(
|
||||
GTK_TOGGLE_BUTTON(child));
|
||||
} else if (strcmp(name, "max_tabs") == 0) {
|
||||
const char *text = gtk_entry_get_text(GTK_ENTRY(child));
|
||||
s->max_tabs = atoi(text);
|
||||
if (s->max_tabs < 1) s->max_tabs = 1;
|
||||
} else if (strcmp(name, "tab_bar_position") == 0) {
|
||||
gint active = gtk_combo_box_get_active(GTK_COMBO_BOX(child));
|
||||
switch (active) {
|
||||
case 0: s->tab_bar_position = GTK_POS_TOP; break;
|
||||
case 1: s->tab_bar_position = GTK_POS_BOTTOM; break;
|
||||
case 2: s->tab_bar_position = GTK_POS_LEFT; break;
|
||||
case 3: s->tab_bar_position = GTK_POS_RIGHT; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_list_free(children);
|
||||
|
||||
/* Rebuild with current history. */
|
||||
int hcount = history_count();
|
||||
if (hcount == 0) {
|
||||
GtkWidget *empty = gtk_menu_item_new_with_label("(no recent pages)");
|
||||
gtk_widget_set_sensitive(empty, FALSE);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty);
|
||||
} else {
|
||||
int show = hcount < 20 ? hcount : 20;
|
||||
for (int i = 0; i < show; i++) {
|
||||
const char *url = history_get(i);
|
||||
if (url == NULL) break;
|
||||
char label[80];
|
||||
if (strlen(url) > 75) {
|
||||
snprintf(label, sizeof(label), "%.72s...", url);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "%s", url);
|
||||
settings_save();
|
||||
tab_manager_apply_settings();
|
||||
g_print("[settings] Saved\n");
|
||||
}
|
||||
GtkWidget *hitem = gtk_menu_item_new_with_label(label);
|
||||
gtk_widget_set_tooltip_text(hitem, url);
|
||||
g_signal_connect(hitem, "activate",
|
||||
G_CALLBACK(on_menu_history_item), webview);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), hitem);
|
||||
}
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
GtkWidget *clear_item = gtk_menu_item_new_with_label("Clear Recents");
|
||||
g_signal_connect(clear_item, "activate",
|
||||
G_CALLBACK(on_menu_history_clear), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item);
|
||||
}
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
gtk_widget_destroy(GTK_WIDGET(dialog));
|
||||
}
|
||||
|
||||
/* Open a local file via file chooser dialog. */
|
||||
static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
|
||||
void on_menu_settings(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
GtkWindow *window = GTK_WINDOW(data);
|
||||
if (window == NULL) return;
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new(
|
||||
"Open File",
|
||||
window,
|
||||
GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
const browser_settings_t *s = settings_get();
|
||||
|
||||
GtkWidget *dialog = gtk_dialog_new_with_buttons(
|
||||
"Settings", window,
|
||||
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT,
|
||||
"_OK", GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
gtk_window_set_default_size(GTK_WINDOW(dialog), 400, 350);
|
||||
|
||||
GtkFileFilter *filter_html = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter_html, "HTML files");
|
||||
gtk_file_filter_add_pattern(filter_html, "*.html");
|
||||
gtk_file_filter_add_pattern(filter_html, "*.htm");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_html);
|
||||
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||||
gtk_container_set_border_width(GTK_CONTAINER(content), 12);
|
||||
GtkBox *vbox = GTK_BOX(content);
|
||||
|
||||
GtkFileFilter *filter_all = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter_all, "All files");
|
||||
gtk_file_filter_add_pattern(filter_all, "*");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_all);
|
||||
/* Restore session checkbox. */
|
||||
GtkWidget *cb_restore = gtk_check_button_new_with_label(
|
||||
"Restore session on startup");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb_restore),
|
||||
s->restore_session);
|
||||
gtk_widget_set_name(cb_restore, "restore_session");
|
||||
gtk_box_pack_start(vbox, cb_restore, FALSE, FALSE, 4);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
||||
char *uri = g_strdup_printf("file://%s", filename);
|
||||
/* Get the web view from the window's data. */
|
||||
WebKitWebView *webview = g_object_get_data(G_OBJECT(window), "webview");
|
||||
if (webview) {
|
||||
webkit_web_view_load_uri(webview, uri);
|
||||
/* New tab URL entry. */
|
||||
GtkWidget *lbl_url = gtk_label_new("New tab URL:");
|
||||
gtk_widget_set_halign(lbl_url, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(vbox, lbl_url, FALSE, FALSE, 4);
|
||||
GtkWidget *entry_url = gtk_entry_new();
|
||||
gtk_entry_set_text(GTK_ENTRY(entry_url), s->new_tab_url);
|
||||
gtk_widget_set_name(entry_url, "new_tab_url");
|
||||
gtk_box_pack_start(vbox, entry_url, FALSE, FALSE, 4);
|
||||
|
||||
/* Tab bar position combo. */
|
||||
GtkWidget *lbl_pos = gtk_label_new("Tab bar position:");
|
||||
gtk_widget_set_halign(lbl_pos, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(vbox, lbl_pos, FALSE, FALSE, 4);
|
||||
GtkWidget *combo_pos = gtk_combo_box_text_new();
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo_pos), NULL, "Top");
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo_pos), NULL, "Bottom");
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo_pos), NULL, "Left");
|
||||
gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo_pos), NULL, "Right");
|
||||
int pos_active = 0;
|
||||
switch (s->tab_bar_position) {
|
||||
case GTK_POS_TOP: pos_active = 0; break;
|
||||
case GTK_POS_BOTTOM: pos_active = 1; break;
|
||||
case GTK_POS_LEFT: pos_active = 2; break;
|
||||
case GTK_POS_RIGHT: pos_active = 3; break;
|
||||
}
|
||||
g_free(uri);
|
||||
g_free(filename);
|
||||
}
|
||||
gtk_widget_destroy(dialog);
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(combo_pos), pos_active);
|
||||
gtk_widget_set_name(combo_pos, "tab_bar_position");
|
||||
gtk_box_pack_start(vbox, combo_pos, FALSE, FALSE, 4);
|
||||
|
||||
/* Show tab close buttons. */
|
||||
GtkWidget *cb_close_btn = gtk_check_button_new_with_label(
|
||||
"Show tab close buttons");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb_close_btn),
|
||||
s->show_tab_close_buttons);
|
||||
gtk_widget_set_name(cb_close_btn, "show_tab_close_buttons");
|
||||
gtk_box_pack_start(vbox, cb_close_btn, FALSE, FALSE, 4);
|
||||
|
||||
/* Middle-click to close. */
|
||||
GtkWidget *cb_mid_click = gtk_check_button_new_with_label(
|
||||
"Middle-click to close tab");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb_mid_click),
|
||||
s->middle_click_close);
|
||||
gtk_widget_set_name(cb_mid_click, "middle_click_close");
|
||||
gtk_box_pack_start(vbox, cb_mid_click, FALSE, FALSE, 4);
|
||||
|
||||
/* Ctrl+Tab switching. */
|
||||
GtkWidget *cb_ctrl_tab = gtk_check_button_new_with_label(
|
||||
"Ctrl+Tab to switch tabs");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb_ctrl_tab),
|
||||
s->ctrl_tab_switch);
|
||||
gtk_widget_set_name(cb_ctrl_tab, "ctrl_tab_switch");
|
||||
gtk_box_pack_start(vbox, cb_ctrl_tab, FALSE, FALSE, 4);
|
||||
|
||||
/* Tab drag reordering. */
|
||||
GtkWidget *cb_drag = gtk_check_button_new_with_label(
|
||||
"Allow drag to reorder tabs");
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb_drag),
|
||||
s->tab_drag_reorder);
|
||||
gtk_widget_set_name(cb_drag, "tab_drag_reorder");
|
||||
gtk_box_pack_start(vbox, cb_drag, FALSE, FALSE, 4);
|
||||
|
||||
/* Max tabs. */
|
||||
GtkWidget *lbl_max = gtk_label_new("Maximum tabs:");
|
||||
gtk_widget_set_halign(lbl_max, GTK_ALIGN_START);
|
||||
gtk_box_pack_start(vbox, lbl_max, FALSE, FALSE, 4);
|
||||
GtkWidget *entry_max = gtk_entry_new();
|
||||
char max_str[16];
|
||||
snprintf(max_str, sizeof(max_str), "%d", s->max_tabs);
|
||||
gtk_entry_set_text(GTK_ENTRY(entry_max), max_str);
|
||||
gtk_widget_set_name(entry_max, "max_tabs");
|
||||
gtk_box_pack_start(vbox, entry_max, FALSE, FALSE, 4);
|
||||
|
||||
g_signal_connect(dialog, "response",
|
||||
G_CALLBACK(on_settings_dialog_response), NULL);
|
||||
|
||||
gtk_widget_show_all(dialog);
|
||||
}
|
||||
|
||||
/* Lock session: clear the signer but keep the saved identity.
|
||||
* The user will need to re-authenticate (switch identity) to sign again. */
|
||||
static void on_menu_lock_session(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_state.signer) {
|
||||
nostr_signer_free(g_state.signer);
|
||||
g_state.signer = NULL;
|
||||
}
|
||||
g_state.readonly = TRUE; /* no signing until re-auth */
|
||||
nostr_bridge_set_signer(NULL, g_state.pubkey_hex, TRUE);
|
||||
g_print("[identity] session locked (signer cleared, identity preserved)\n");
|
||||
}
|
||||
/* ---- Keyboard shortcuts --------------------------------------------- */
|
||||
|
||||
/* Build the hamburger menu. */
|
||||
static GtkWidget *build_hamburger_menu(GtkWidget *window,
|
||||
WebKitWebView *webview) {
|
||||
GtkWidget *menu = gtk_menu_new();
|
||||
|
||||
/* Navigation group. */
|
||||
GtkWidget *item_open = gtk_menu_item_new_with_label("Open File…");
|
||||
GtkWidget *item_reload = gtk_menu_item_new_with_label("Reload");
|
||||
GtkWidget *item_stop = gtk_menu_item_new_with_label("Stop");
|
||||
g_signal_connect(item_open, "activate", G_CALLBACK(on_menu_open_file),
|
||||
window);
|
||||
g_signal_connect(item_reload, "activate", G_CALLBACK(on_menu_reload),
|
||||
webview);
|
||||
g_signal_connect(item_stop, "activate", G_CALLBACK(on_menu_stop), webview);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_open);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_stop);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Recents submenu — rebuilt dynamically each time it's shown. */
|
||||
GtkWidget *history_menu = gtk_menu_new();
|
||||
g_signal_connect(history_menu, "show", G_CALLBACK(on_history_menu_show),
|
||||
webview);
|
||||
|
||||
GtkWidget *item_history = gtk_menu_item_new_with_label("Recents");
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_history), history_menu);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_history);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Identity group — show current identity status. */
|
||||
char identity_label[80];
|
||||
const char *method_name = "none";
|
||||
switch (g_state.method) {
|
||||
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;
|
||||
default: break;
|
||||
}
|
||||
if (g_state.pubkey_hex[0]) {
|
||||
/* Show first 8 chars of pubkey for identification. */
|
||||
char short_pubkey[12];
|
||||
memcpy(short_pubkey, g_state.pubkey_hex, 8);
|
||||
short_pubkey[8] = '\0';
|
||||
snprintf(identity_label, sizeof(identity_label),
|
||||
"Identity: %s… (%s)", short_pubkey, method_name);
|
||||
} else {
|
||||
snprintf(identity_label, sizeof(identity_label), "Identity: none");
|
||||
}
|
||||
GtkWidget *item_identity = gtk_menu_item_new_with_label(identity_label);
|
||||
gtk_widget_set_sensitive(item_identity, FALSE);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_identity);
|
||||
|
||||
GtkWidget *item_switch = gtk_menu_item_new_with_label("Switch Identity…");
|
||||
GtkWidget *item_lock = gtk_menu_item_new_with_label("Lock Session");
|
||||
GtkWidget *item_logout = gtk_menu_item_new_with_label("Logout");
|
||||
g_signal_connect(item_switch, "activate", G_CALLBACK(on_menu_switch_identity),
|
||||
window);
|
||||
g_signal_connect(item_lock, "activate", G_CALLBACK(on_menu_lock_session), NULL);
|
||||
g_signal_connect(item_logout, "activate", G_CALLBACK(on_menu_logout), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_switch);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_lock);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_logout);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Roadmap group. */
|
||||
GtkWidget *item_security =
|
||||
gtk_check_menu_item_new_with_label("Security strip (SOP/CORS/certs)");
|
||||
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_security), FALSE);
|
||||
g_signal_connect(item_security, "toggled",
|
||||
G_CALLBACK(on_menu_security_strip), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_security);
|
||||
|
||||
GtkWidget *item_fips = gtk_menu_item_new_with_label("FIPS URI scheme…");
|
||||
GtkWidget *item_nostr = gtk_menu_item_new_with_label("Nostr signing status");
|
||||
g_signal_connect(item_fips, "activate", G_CALLBACK(on_menu_fips), NULL);
|
||||
g_signal_connect(item_nostr, "activate", G_CALLBACK(on_menu_nostr_sign),
|
||||
NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_fips);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_nostr);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
GtkWidget *item_inspector = gtk_menu_item_new_with_label("Toggle Inspector");
|
||||
g_signal_connect(item_inspector, "activate", G_CALLBACK(on_menu_inspector),
|
||||
webview);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_inspector);
|
||||
|
||||
GtkWidget *item_about = gtk_menu_item_new_with_label("About");
|
||||
g_signal_connect(item_about, "activate", G_CALLBACK(on_menu_about),
|
||||
window);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_about);
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
|
||||
GtkWidget *button = gtk_menu_button_new();
|
||||
gtk_menu_button_set_popup(GTK_MENU_BUTTON(button), menu);
|
||||
gtk_button_set_image(
|
||||
GTK_BUTTON(button),
|
||||
gtk_image_new_from_icon_name("open-menu-symbolic",
|
||||
GTK_ICON_SIZE_BUTTON));
|
||||
gtk_widget_set_tooltip_text(button, "Menu");
|
||||
return button;
|
||||
}
|
||||
|
||||
/* Normalize a bare string into a loadable URL. */
|
||||
static char *normalize_url(const char *input) {
|
||||
if (input == NULL || input[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
if (strstr(input, "://") != NULL) {
|
||||
return g_strdup(input);
|
||||
}
|
||||
return g_strdup_printf("https://%s", input);
|
||||
}
|
||||
|
||||
static void on_url_activate(GtkEntry *entry, WebKitWebView *webview) {
|
||||
const char *text = gtk_entry_get_text(entry);
|
||||
char *url = normalize_url(text);
|
||||
if (url != NULL) {
|
||||
webkit_web_view_load_uri(webview, url);
|
||||
g_free(url);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_load_changed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
GtkEntry *url_entry) {
|
||||
if (load_event == WEBKIT_LOAD_COMMITTED) {
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
if (uri != NULL) {
|
||||
gtk_entry_set_text(url_entry, uri);
|
||||
}
|
||||
} else if (load_event == WEBKIT_LOAD_FINISHED) {
|
||||
const gchar *title = webkit_web_view_get_title(webview);
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
g_print("[loaded] %s -- title: %s\n",
|
||||
uri ? uri : "(null)",
|
||||
(title && title[0]) ? title : "(none)");
|
||||
/* Add to history on successful page load. */
|
||||
if (uri != NULL && uri[0] != '\0') {
|
||||
history_add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean on_load_failed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
gchar *failing_uri,
|
||||
GError *error,
|
||||
static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
gpointer data) {
|
||||
(void)webview;
|
||||
(void)load_event;
|
||||
(void)widget;
|
||||
(void)data;
|
||||
g_print("[failed] %s -- %s\n",
|
||||
failing_uri ? failing_uri : "(null)",
|
||||
error ? error->message : "(unknown)");
|
||||
|
||||
const browser_settings_t *s = settings_get();
|
||||
guint mods = event->state & gtk_accelerator_get_default_mod_mask();
|
||||
|
||||
/* Ctrl+T — new tab. */
|
||||
if ((mods & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_t) {
|
||||
tab_manager_new_tab(NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Ctrl+W — close active tab. */
|
||||
if ((mods & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_w) {
|
||||
tab_manager_close_active();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Ctrl+L — focus URL bar of active tab. */
|
||||
if ((mods & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_l) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab && tab->url_entry) {
|
||||
gtk_widget_grab_focus(tab->url_entry);
|
||||
gtk_editable_select_region(GTK_EDITABLE(tab->url_entry), 0, -1);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Ctrl+Tab / Ctrl+Shift+Tab — cycle tabs. */
|
||||
if (s->ctrl_tab_switch && (mods & GDK_CONTROL_MASK)) {
|
||||
if (event->keyval == GDK_KEY_Tab) {
|
||||
if (mods & GDK_SHIFT_MASK) {
|
||||
tab_manager_prev();
|
||||
} else {
|
||||
tab_manager_next();
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
if (event->keyval == GDK_KEY_ISO_Left_Tab) {
|
||||
tab_manager_prev();
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ctrl+PageDown — next tab. */
|
||||
if ((mods & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_Page_Down) {
|
||||
tab_manager_next();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Ctrl+PageUp — previous tab. */
|
||||
if ((mods & GDK_CONTROL_MASK) && event->keyval == GDK_KEY_Page_Up) {
|
||||
tab_manager_prev();
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* ---- Window destroy ------------------------------------------------- */
|
||||
|
||||
static void on_window_destroy(GtkWidget *widget, gpointer data) {
|
||||
(void)widget;
|
||||
(void)data;
|
||||
|
||||
/* Save the session before cleanup. */
|
||||
session_save();
|
||||
|
||||
if (g_state.signer) {
|
||||
nostr_signer_free(g_state.signer);
|
||||
g_state.signer = NULL;
|
||||
@@ -451,22 +398,17 @@ static void on_window_destroy(GtkWidget *widget, gpointer data) {
|
||||
gtk_main_quit();
|
||||
}
|
||||
|
||||
/* ---- Login flow ------------------------------------------------------ *
|
||||
* Try to restore a saved identity. If none, show the login dialog.
|
||||
* Returns 0 on success (signer or readonly loaded), -1 if user cancelled.
|
||||
*/
|
||||
/* ---- Login flow ----------------------------------------------------- */
|
||||
|
||||
static int do_login(GtkWindow *parent) {
|
||||
/* Initialize nostr_core_lib. */
|
||||
if (nostr_init() != NOSTR_SUCCESS) {
|
||||
g_printerr("[login] Failed to initialize nostr_core_lib\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Show the login dialog every time — no plaintext key persistence.
|
||||
* Encrypted key storage will be added in a future phase. */
|
||||
login_result_t result;
|
||||
if (login_dialog_run(parent, &result) != 0) {
|
||||
return -1; /* cancelled */
|
||||
return -1;
|
||||
}
|
||||
|
||||
g_state.signer = result.signer;
|
||||
@@ -480,127 +422,87 @@ static int do_login(GtkWindow *parent) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- Main ------------------------------------------------------------ */
|
||||
/* ---- Main ----------------------------------------------------------- */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
/* Ignore SIGPIPE — n_signer transports (qrexec, unix, tcp) may close
|
||||
* pipes abruptly, and we don't want that to kill the browser. */
|
||||
(void)signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
gtk_init(&argc, &argv);
|
||||
|
||||
/* Load URL history from disk. */
|
||||
/* Load settings and history from disk. */
|
||||
settings_load();
|
||||
history_load();
|
||||
|
||||
const char *start_url = (argc > 1) ? argv[1] : "https://example.com";
|
||||
char *normalized = normalize_url(start_url);
|
||||
const char *start_url = (argc > 1) ? argv[1] : NULL;
|
||||
|
||||
/* Top-level window. */
|
||||
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(window), "sovereign browser " SB_VERSION);
|
||||
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
|
||||
g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);
|
||||
g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);
|
||||
g_window = GTK_WINDOW(window);
|
||||
|
||||
/* Login before showing the browser. */
|
||||
if (do_login(GTK_WINDOW(window)) != 0) {
|
||||
/* User cancelled login — exit without gtk_main_quit (we haven't
|
||||
* entered gtk_main yet, so calling gtk_main_quit would crash). */
|
||||
if (do_login(g_window) != 0) {
|
||||
g_print("[login] Cancelled, exiting.\n");
|
||||
nostr_cleanup();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/* Vertical box: toolbar on top, web view below. */
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(window), vbox);
|
||||
/* Use the default WebKitWebContext — it comes with proper networking
|
||||
* (cookies, cache, soup session) that a freshly created context lacks.
|
||||
* All tabs will create webviews from this shared context. */
|
||||
WebKitWebContext *web_ctx = webkit_web_context_get_default();
|
||||
|
||||
WebKitWebView *webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
|
||||
|
||||
/* ── Security strip: disable web security restrictions ───────
|
||||
* The "reckless browser" thesis: identity and transport are handled
|
||||
* at a different layer (Nostr keys + FIPS mesh), so the browser's
|
||||
* own security sandbox (CORS, SOP, TLS cert enforcement, mixed
|
||||
* content blocking) gets in the way and is deliberately removed.
|
||||
*/
|
||||
|
||||
/* Enable developer extras (Web Inspector) for debugging. */
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(webview);
|
||||
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);
|
||||
|
||||
/* Allow file:// pages to access other file:// resources and make
|
||||
* cross-origin requests (needed for our test page to call
|
||||
* sovereign://). */
|
||||
webkit_settings_set_allow_file_access_from_file_urls(settings, TRUE);
|
||||
webkit_settings_set_allow_universal_access_from_file_urls(settings, TRUE);
|
||||
|
||||
/* Disable mixed content blocking — allow HTTP resources on HTTPS
|
||||
* pages (FIPS/nostr:// content may not use TLS). */
|
||||
webkit_settings_set_allow_modal_dialogs(settings, TRUE);
|
||||
|
||||
/* Get the web context for scheme + security manager registration. */
|
||||
WebKitWebContext *web_ctx = webkit_web_view_get_context(webview);
|
||||
|
||||
/* Register sovereign:// as a secure, local, CORS-enabled scheme so
|
||||
* that fetch() from any page can call it without cross-origin
|
||||
* restrictions. */
|
||||
WebKitSecurityManager *sec_mgr = webkit_web_context_get_security_manager(web_ctx);
|
||||
/* ── Security strip: disable web security restrictions ─────── */
|
||||
WebKitSecurityManager *sec_mgr =
|
||||
webkit_web_context_get_security_manager(web_ctx);
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "sovereign");
|
||||
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "sovereign");
|
||||
webkit_security_manager_register_uri_scheme_as_cors_enabled(sec_mgr, "sovereign");
|
||||
|
||||
/* Also register file:// as secure so local pages can call our bridge. */
|
||||
webkit_security_manager_register_uri_scheme_as_cors_enabled(sec_mgr,
|
||||
"sovereign");
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "file");
|
||||
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "file");
|
||||
|
||||
/* Accept any TLS certificate (FIPS uses Noise IK, not TLS CAs). */
|
||||
WebKitWebsiteDataManager *data_mgr = webkit_web_context_get_website_data_manager(web_ctx);
|
||||
WebKitWebsiteDataManager *data_mgr =
|
||||
webkit_web_context_get_website_data_manager(web_ctx);
|
||||
webkit_website_data_manager_set_tls_errors_policy(
|
||||
data_mgr, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
|
||||
|
||||
/* Register the sovereign:// URI scheme for the window.nostr bridge
|
||||
* and browser-internal pages (sovereign://security). */
|
||||
/* Register the sovereign:// URI scheme for the window.nostr bridge. */
|
||||
nostr_bridge_register(web_ctx, g_state.signer, g_state.pubkey_hex,
|
||||
g_state.readonly);
|
||||
|
||||
/* Vertical box: the tab manager's notebook fills the window. */
|
||||
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_container_add(GTK_CONTAINER(window), vbox);
|
||||
|
||||
/* Initialize the tab manager. */
|
||||
tab_manager_init(GTK_CONTAINER(vbox), web_ctx, g_window);
|
||||
|
||||
/* Try to restore the previous session. If nothing was restored,
|
||||
* open a single tab with the start URL or the default new-tab URL. */
|
||||
int restored = session_restore();
|
||||
if (restored == 0) {
|
||||
const char *url = start_url;
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
url = settings_get()->new_tab_url;
|
||||
}
|
||||
tab_manager_new_tab(url);
|
||||
}
|
||||
|
||||
/* Wire the security refs to the first tab's settings (settings are
|
||||
* per-webview, not per-context). The sovereign://security page uses
|
||||
* these to read and toggle security features. */
|
||||
{
|
||||
tab_info_t *first_tab = tab_manager_get(0);
|
||||
if (first_tab && first_tab->webview) {
|
||||
WebKitSettings *settings =
|
||||
webkit_web_view_get_settings(first_tab->webview);
|
||||
nostr_bridge_set_security_refs(settings, sec_mgr);
|
||||
|
||||
/* Inject window.nostr into every page before page scripts run. */
|
||||
nostr_inject_setup(webview);
|
||||
|
||||
/* Toolbar: [hamburger] [url entry], horizontal. */
|
||||
GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_top(toolbar, 4);
|
||||
gtk_widget_set_margin_bottom(toolbar, 4);
|
||||
gtk_widget_set_margin_start(toolbar, 4);
|
||||
gtk_widget_set_margin_end(toolbar, 4);
|
||||
gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *hamburger = build_hamburger_menu(window, webview);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar), hamburger, FALSE, FALSE, 0);
|
||||
|
||||
GtkWidget *url_entry = gtk_entry_new();
|
||||
gtk_entry_set_text(GTK_ENTRY(url_entry),
|
||||
normalized ? normalized : start_url);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar), url_entry, TRUE, TRUE, 0);
|
||||
|
||||
/* Store webview reference on the window for menu callbacks. */
|
||||
g_object_set_data(G_OBJECT(window), "webview", webview);
|
||||
|
||||
gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(webview), TRUE, TRUE, 0);
|
||||
|
||||
/* Wire signals. */
|
||||
g_signal_connect(url_entry, "activate", G_CALLBACK(on_url_activate),
|
||||
webview);
|
||||
g_signal_connect(webview, "load-changed", G_CALLBACK(on_load_changed),
|
||||
url_entry);
|
||||
g_signal_connect(webview, "load-failed", G_CALLBACK(on_load_failed),
|
||||
NULL);
|
||||
|
||||
/* Load the start URL. */
|
||||
if (normalized != NULL) {
|
||||
webkit_web_view_load_uri(webview, normalized);
|
||||
g_free(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_widget_show_all(window);
|
||||
|
||||
108
src/session.c
Normal file
108
src/session.c
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* session.c — tab session save/restore for sovereign_browser
|
||||
*
|
||||
* Saves open tab URLs to ~/.sovereign_browser/session.txt (one per line)
|
||||
* and restores them on startup if settings.restore_session is true.
|
||||
*/
|
||||
|
||||
#include "session.h"
|
||||
#include "settings.h"
|
||||
#include "tab_manager.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define SESSION_URL_MAX 2048
|
||||
|
||||
/* ── Path helper ──────────────────────────────────────────────────── */
|
||||
|
||||
static int session_path(char *out, size_t out_sz) {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == NULL || home[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char dir[512];
|
||||
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
|
||||
if (n < 0 || (size_t)n >= sizeof(dir)) {
|
||||
return -1;
|
||||
}
|
||||
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = snprintf(out, out_sz, "%s/session.txt", dir);
|
||||
if (n < 0 || (size_t)n >= out_sz) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Save ─────────────────────────────────────────────────────────── */
|
||||
|
||||
void session_save(void) {
|
||||
char path[512];
|
||||
if (session_path(path, sizeof(path)) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
fchmod(fileno(f), 0600);
|
||||
|
||||
int count = tab_manager_count();
|
||||
for (int i = 0; i < count; i++) {
|
||||
const char *url = tab_manager_get_url(i);
|
||||
if (url != NULL && url[0] != '\0') {
|
||||
fprintf(f, "%s\n", url);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
g_print("[session] Saved %d tab(s)\n", count);
|
||||
}
|
||||
|
||||
/* ── Restore ──────────────────────────────────────────────────────── */
|
||||
|
||||
int session_restore(void) {
|
||||
const browser_settings_t *s = settings_get();
|
||||
if (!s->restore_session) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char path[512];
|
||||
if (session_path(path, sizeof(path)) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int restored = 0;
|
||||
char line[SESSION_URL_MAX];
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
/* Strip newline. */
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||
line[--len] = '\0';
|
||||
}
|
||||
if (len == 0) continue;
|
||||
|
||||
int index = tab_manager_new_tab(line);
|
||||
if (index >= 0) {
|
||||
restored++;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
g_print("[session] Restored %d tab(s)\n", restored);
|
||||
return restored;
|
||||
}
|
||||
35
src/session.h
Normal file
35
src/session.h
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* session.h — tab session save/restore for sovereign_browser
|
||||
*
|
||||
* Saves the list of open tab URLs to ~/.sovereign_browser/session.txt
|
||||
* on window close, and restores them on startup if settings.restore_session
|
||||
* is true.
|
||||
*/
|
||||
|
||||
#ifndef SESSION_H
|
||||
#define SESSION_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Save the current set of open tab URLs to disk.
|
||||
* Called on window destroy (before gtk_main_quit).
|
||||
*/
|
||||
void session_save(void);
|
||||
|
||||
/*
|
||||
* Restore the saved session by creating a tab for each URL in
|
||||
* ~/.sovereign_browser/session.txt.
|
||||
*
|
||||
* Returns the number of tabs created, or 0 if no session was restored
|
||||
* (file missing, empty, or restore_session setting is false).
|
||||
*/
|
||||
int session_restore(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SESSION_H */
|
||||
201
src/settings.c
Normal file
201
src/settings.c
Normal file
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* settings.c — browser preferences for sovereign_browser
|
||||
*
|
||||
* Persists settings to ~/.sovereign_browser/settings.conf as key=value lines.
|
||||
*/
|
||||
|
||||
#include "settings.h"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp */
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ── Global singleton ─────────────────────────────────────────────── */
|
||||
|
||||
static browser_settings_t g_settings;
|
||||
|
||||
/* ── Defaults ─────────────────────────────────────────────────────── */
|
||||
|
||||
static void settings_set_defaults(browser_settings_t *s) {
|
||||
s->restore_session = TRUE;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ── Path helper ──────────────────────────────────────────────────── */
|
||||
|
||||
static int settings_path(char *out, size_t out_sz) {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == NULL || home[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
char dir[512];
|
||||
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
|
||||
if (n < 0 || (size_t)n >= sizeof(dir)) {
|
||||
return -1;
|
||||
}
|
||||
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
n = snprintf(out, out_sz, "%s/settings.conf", dir);
|
||||
if (n < 0 || (size_t)n >= out_sz) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── 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);
|
||||
}
|
||||
|
||||
/* ── Load / Save ──────────────────────────────────────────────────── */
|
||||
|
||||
void settings_load(void) {
|
||||
settings_set_defaults(&g_settings);
|
||||
|
||||
char path[512];
|
||||
if (settings_path(path, sizeof(path)) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), f) != NULL) {
|
||||
/* Strip newline. */
|
||||
size_t len = strlen(line);
|
||||
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||
line[--len] = '\0';
|
||||
}
|
||||
if (len == 0 || line[0] == '#') continue;
|
||||
|
||||
/* Split key=value. */
|
||||
char *eq = strchr(line, '=');
|
||||
if (eq == NULL) continue;
|
||||
*eq = '\0';
|
||||
char *key = line;
|
||||
char *value = eq + 1;
|
||||
|
||||
/* Trim whitespace from key. */
|
||||
while (*key == ' ' || *key == '\t') key++;
|
||||
char *kend = key + strlen(key);
|
||||
while (kend > key && (kend[-1] == ' ' || kend[-1] == '\t')) *--kend = '\0';
|
||||
|
||||
/* Trim whitespace from value. */
|
||||
while (*value == ' ' || *value == '\t') value++;
|
||||
|
||||
if (strcmp(key, "restore_session") == 0) {
|
||||
g_settings.restore_session = parse_bool(value, g_settings.restore_session);
|
||||
} else if (strcmp(key, "new_tab_url") == 0) {
|
||||
snprintf(g_settings.new_tab_url, sizeof(g_settings.new_tab_url),
|
||||
"%s", value);
|
||||
} else if (strcmp(key, "tab_bar_position") == 0) {
|
||||
g_settings.tab_bar_position = parse_position(value, g_settings.tab_bar_position);
|
||||
} else if (strcmp(key, "show_tab_close_buttons") == 0) {
|
||||
g_settings.show_tab_close_buttons = parse_bool(value, g_settings.show_tab_close_buttons);
|
||||
} else if (strcmp(key, "middle_click_close") == 0) {
|
||||
g_settings.middle_click_close = parse_bool(value, g_settings.middle_click_close);
|
||||
} else if (strcmp(key, "ctrl_tab_switch") == 0) {
|
||||
g_settings.ctrl_tab_switch = parse_bool(value, g_settings.ctrl_tab_switch);
|
||||
} else if (strcmp(key, "max_tabs") == 0) {
|
||||
g_settings.max_tabs = parse_int(value, g_settings.max_tabs);
|
||||
if (g_settings.max_tabs < 1) g_settings.max_tabs = 1;
|
||||
} else if (strcmp(key, "tab_drag_reorder") == 0) {
|
||||
g_settings.tab_drag_reorder = parse_bool(value, g_settings.tab_drag_reorder);
|
||||
}
|
||||
/* Unknown keys are silently ignored. */
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void settings_save(void) {
|
||||
char path[512];
|
||||
if (settings_path(path, sizeof(path)) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
fchmod(fileno(f), 0600);
|
||||
|
||||
const char *pos_str = "top";
|
||||
switch (g_settings.tab_bar_position) {
|
||||
case GTK_POS_TOP: pos_str = "top"; break;
|
||||
case GTK_POS_BOTTOM: pos_str = "bottom"; break;
|
||||
case GTK_POS_LEFT: pos_str = "left"; break;
|
||||
case GTK_POS_RIGHT: pos_str = "right"; break;
|
||||
}
|
||||
|
||||
fprintf(f, "# sovereign_browser settings\n");
|
||||
fprintf(f, "restore_session=%s\n", g_settings.restore_session ? "true" : "false");
|
||||
fprintf(f, "new_tab_url=%s\n", g_settings.new_tab_url);
|
||||
fprintf(f, "tab_bar_position=%s\n", pos_str);
|
||||
fprintf(f, "show_tab_close_buttons=%s\n", g_settings.show_tab_close_buttons ? "true" : "false");
|
||||
fprintf(f, "middle_click_close=%s\n", g_settings.middle_click_close ? "true" : "false");
|
||||
fprintf(f, "ctrl_tab_switch=%s\n", g_settings.ctrl_tab_switch ? "true" : "false");
|
||||
fprintf(f, "max_tabs=%d\n", g_settings.max_tabs);
|
||||
fprintf(f, "tab_drag_reorder=%s\n", g_settings.tab_drag_reorder ? "true" : "false");
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
const browser_settings_t *settings_get(void) {
|
||||
return &g_settings;
|
||||
}
|
||||
|
||||
browser_settings_t *settings_get_mutable(void) {
|
||||
return &g_settings;
|
||||
}
|
||||
61
src/settings.h
Normal file
61
src/settings.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* settings.h — browser preferences for sovereign_browser
|
||||
*
|
||||
* Persists user-configurable settings to ~/.sovereign_browser/settings.conf
|
||||
* as simple key=value lines. A global singleton is loaded at startup and
|
||||
* accessed via settings_get().
|
||||
*/
|
||||
|
||||
#ifndef SETTINGS_H
|
||||
#define SETTINGS_H
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SETTINGS_NEW_TAB_URL_DEFAULT "https://example.com"
|
||||
#define SETTINGS_NEW_TAB_URL_MAX 512
|
||||
#define SETTINGS_MAX_TABS_DEFAULT 50
|
||||
|
||||
typedef struct {
|
||||
gboolean restore_session; /* restore open tabs on next launch */
|
||||
char new_tab_url[SETTINGS_NEW_TAB_URL_MAX];
|
||||
int tab_bar_position; /* GTK_POS_TOP / BOTTOM / LEFT / RIGHT */
|
||||
gboolean show_tab_close_buttons; /* show per-tab close button */
|
||||
gboolean middle_click_close; /* middle-click on tab closes it */
|
||||
gboolean ctrl_tab_switch; /* Ctrl+Tab cycles tabs */
|
||||
int max_tabs; /* maximum simultaneous tabs */
|
||||
gboolean tab_drag_reorder; /* allow drag to reorder tabs */
|
||||
} 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.
|
||||
*/
|
||||
void settings_load(void);
|
||||
|
||||
/*
|
||||
* Save the current global settings to disk.
|
||||
*/
|
||||
void settings_save(void);
|
||||
|
||||
/*
|
||||
* Get a pointer to the global settings singleton.
|
||||
* Always valid after settings_load().
|
||||
*/
|
||||
const browser_settings_t *settings_get(void);
|
||||
|
||||
/*
|
||||
* Get a mutable pointer to the global settings.
|
||||
* Used by the settings dialog to modify values; call settings_save() after.
|
||||
*/
|
||||
browser_settings_t *settings_get_mutable(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SETTINGS_H */
|
||||
783
src/tab_manager.c
Normal file
783
src/tab_manager.c
Normal file
@@ -0,0 +1,783 @@
|
||||
/*
|
||||
* tab_manager.c — multi-tab management for sovereign_browser
|
||||
*
|
||||
* Each tab is a GtkBox page containing a per-tab toolbar (hamburger menu +
|
||||
* URL entry) and a WebKitWebView. All webviews share the same
|
||||
* WebKitWebContext so the sovereign:// bridge and security settings apply
|
||||
* to every tab automatically.
|
||||
*/
|
||||
|
||||
#include "tab_manager.h"
|
||||
#include "settings.h"
|
||||
#include "nostr_inject.h"
|
||||
#include "history.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── External callbacks defined in main.c ─────────────────────────── *
|
||||
* These handle identity-related menu actions that need access to the
|
||||
* global app_state_t (signer, pubkey, method). main.c owns that state.
|
||||
*/
|
||||
/* Proxy functions defined in main.c that wrap the app_state_t-aware
|
||||
* callbacks so they match GTK signal handler signatures. */
|
||||
extern void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void app_menu_lock_session_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void app_menu_logout_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data);
|
||||
extern void app_menu_fips_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void app_menu_about_proxy(GtkMenuItem *item, gpointer data);
|
||||
extern void on_menu_settings(GtkMenuItem *item, gpointer data);
|
||||
|
||||
/* ── Static state ─────────────────────────────────────────────────── */
|
||||
|
||||
static GtkWidget *g_notebook = NULL;
|
||||
static WebKitWebContext *g_ctx = NULL;
|
||||
static GtkWindow *g_window = 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;
|
||||
static int g_tab_cap = 0;
|
||||
|
||||
/* ── Forward declarations ─────────────────────────────────────────── */
|
||||
|
||||
static tab_info_t *tab_create(const char *url);
|
||||
static GtkWidget *build_tab_label(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);
|
||||
|
||||
/* ── URL helper (same logic as main.c's normalize_url) ────────────── */
|
||||
|
||||
static char *normalize_url(const char *input) {
|
||||
if (input == NULL || input[0] == '\0') {
|
||||
return NULL;
|
||||
}
|
||||
if (strstr(input, "://") != NULL) {
|
||||
return g_strdup(input);
|
||||
}
|
||||
return g_strdup_printf("https://%s", input);
|
||||
}
|
||||
|
||||
/* ── Tab label widget ─────────────────────────────────────────────── */
|
||||
|
||||
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);
|
||||
if (index >= 0) {
|
||||
tab_manager_close_tab(index);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean on_tab_label_button_press(GtkWidget *widget,
|
||||
GdkEventButton *event,
|
||||
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);
|
||||
|
||||
if (index < 0) return FALSE;
|
||||
|
||||
const browser_settings_t *s = settings_get();
|
||||
|
||||
/* Middle-click to close. */
|
||||
if (event->button == 2 && s->middle_click_close) {
|
||||
tab_manager_close_tab(index);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Right-click context menu. */
|
||||
if (event->button == 3) {
|
||||
GtkWidget *menu = gtk_menu_new();
|
||||
|
||||
GtkWidget *item_new = gtk_menu_item_new_with_label("New Tab");
|
||||
g_signal_connect(item_new, "activate",
|
||||
G_CALLBACK(on_tab_close_clicked_proxy_new), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_new);
|
||||
|
||||
GtkWidget *item_close = gtk_menu_item_new_with_label("Close Tab");
|
||||
g_signal_connect_swapped(item_close, "activate",
|
||||
G_CALLBACK(tab_manager_close_tab),
|
||||
GINT_TO_POINTER(index));
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close);
|
||||
|
||||
GtkWidget *item_close_others =
|
||||
gtk_menu_item_new_with_label("Close Other Tabs");
|
||||
g_signal_connect_swapped(item_close_others, "activate",
|
||||
G_CALLBACK(tab_manager_close_others),
|
||||
GINT_TO_POINTER(index));
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close_others);
|
||||
|
||||
GtkWidget *item_close_right =
|
||||
gtk_menu_item_new_with_label("Close Tabs to the Right");
|
||||
g_signal_connect_swapped(item_close_right, "activate",
|
||||
G_CALLBACK(tab_manager_close_to_right),
|
||||
GINT_TO_POINTER(index));
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_close_right);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
GtkWidget *item_dup = gtk_menu_item_new_with_label("Duplicate Tab");
|
||||
g_signal_connect_swapped(item_dup, "activate",
|
||||
G_CALLBACK(tab_manager_duplicate),
|
||||
GINT_TO_POINTER(index));
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_dup);
|
||||
|
||||
GtkWidget *item_reload = gtk_menu_item_new_with_label("Reload Tab");
|
||||
g_signal_connect_swapped(item_reload, "activate",
|
||||
G_CALLBACK(tab_manager_reload),
|
||||
GINT_TO_POINTER(index));
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload);
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Proxy callback for "New Tab" in the context menu — just calls new_tab. */
|
||||
static void on_tab_close_clicked_proxy_new(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
tab_manager_new_tab(NULL);
|
||||
}
|
||||
|
||||
static GtkWidget *build_tab_label(tab_info_t *tab) {
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
|
||||
/* Favicon placeholder icon. */
|
||||
GtkWidget *icon = gtk_image_new_from_icon_name("text-html",
|
||||
GTK_ICON_SIZE_MENU);
|
||||
gtk_box_pack_start(GTK_BOX(box), icon, FALSE, FALSE, 0);
|
||||
|
||||
/* Title label. */
|
||||
tab->title_label = gtk_label_new("New Tab");
|
||||
gtk_label_set_ellipsize(GTK_LABEL(tab->title_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(tab->title_label), 20);
|
||||
gtk_box_pack_start(GTK_BOX(box), tab->title_label, TRUE, TRUE, 0);
|
||||
|
||||
/* Close button. */
|
||||
const browser_settings_t *s = settings_get();
|
||||
if (s->show_tab_close_buttons) {
|
||||
GtkWidget *close_btn = gtk_button_new();
|
||||
gtk_button_set_relief(GTK_BUTTON(close_btn), GTK_RELIEF_NONE);
|
||||
gtk_button_set_image(GTK_BUTTON(close_btn),
|
||||
gtk_image_new_from_icon_name("window-close-symbolic",
|
||||
GTK_ICON_SIZE_MENU));
|
||||
gtk_widget_set_tooltip_text(close_btn, "Close tab");
|
||||
g_signal_connect(close_btn, "clicked",
|
||||
G_CALLBACK(on_tab_close_clicked), tab);
|
||||
gtk_box_pack_start(GTK_BOX(box), close_btn, FALSE, FALSE, 0);
|
||||
}
|
||||
|
||||
/* Connect button-press for middle-click and right-click on the label. */
|
||||
gtk_widget_add_events(box, GDK_BUTTON_PRESS_MASK);
|
||||
g_signal_connect(box, "button-press-event",
|
||||
G_CALLBACK(on_tab_label_button_press), tab);
|
||||
|
||||
gtk_widget_show_all(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
/* ── Per-tab signal handlers ──────────────────────────────────────── */
|
||||
|
||||
static void on_url_activate(GtkEntry *entry, gpointer user_data) {
|
||||
tab_info_t *tab = (tab_info_t *)user_data;
|
||||
const char *text = gtk_entry_get_text(entry);
|
||||
char *url = normalize_url(text);
|
||||
if (url != NULL) {
|
||||
webkit_web_view_load_uri(tab->webview, url);
|
||||
g_free(url);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_load_changed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
gpointer user_data) {
|
||||
tab_info_t *tab = (tab_info_t *)user_data;
|
||||
|
||||
if (load_event == WEBKIT_LOAD_COMMITTED) {
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
if (uri != NULL) {
|
||||
gtk_entry_set_text(GTK_ENTRY(tab->url_entry), uri);
|
||||
snprintf(tab->current_url, sizeof(tab->current_url), "%s", uri);
|
||||
}
|
||||
} else if (load_event == WEBKIT_LOAD_FINISHED) {
|
||||
const gchar *title = webkit_web_view_get_title(webview);
|
||||
const gchar *uri = webkit_web_view_get_uri(webview);
|
||||
g_print("[loaded] %s -- title: %s\n",
|
||||
uri ? uri : "(null)",
|
||||
(title && title[0]) ? title : "(none)");
|
||||
|
||||
/* Update tab title. */
|
||||
if (title && title[0]) {
|
||||
int index = gtk_notebook_page_num(GTK_NOTEBOOK(g_notebook),
|
||||
tab->page);
|
||||
if (index >= 0) {
|
||||
tab_manager_set_title(index, title);
|
||||
}
|
||||
}
|
||||
|
||||
/* Add to history. */
|
||||
if (uri != NULL && uri[0] != '\0') {
|
||||
history_add(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean on_load_failed(WebKitWebView *webview,
|
||||
WebKitLoadEvent load_event,
|
||||
gchar *failing_uri,
|
||||
GError *error,
|
||||
gpointer data) {
|
||||
(void)webview;
|
||||
(void)load_event;
|
||||
(void)data;
|
||||
g_print("[failed] %s -- %s\n",
|
||||
failing_uri ? failing_uri : "(null)",
|
||||
error ? error->message : "(unknown)");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* ── Hamburger menu callbacks (webview-specific) ──────────────────── */
|
||||
|
||||
static void on_menu_reload(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab && tab->webview) {
|
||||
webkit_web_view_reload_bypass_cache(tab->webview);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_stop(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab && tab->webview) {
|
||||
webkit_web_view_stop_loading(tab->webview);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_inspector(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab && tab->webview) {
|
||||
WebKitWebInspector *insp = webkit_web_view_get_inspector(tab->webview);
|
||||
if (insp) webkit_web_inspector_show(insp);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_open_file(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
if (g_window == NULL) return;
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new(
|
||||
"Open File", g_window, GTK_FILE_CHOOSER_ACTION_OPEN,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Open", GTK_RESPONSE_ACCEPT, NULL);
|
||||
|
||||
GtkFileFilter *filter_html = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter_html, "HTML files");
|
||||
gtk_file_filter_add_pattern(filter_html, "*.html");
|
||||
gtk_file_filter_add_pattern(filter_html, "*.htm");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_html);
|
||||
|
||||
GtkFileFilter *filter_all = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter_all, "All files");
|
||||
gtk_file_filter_add_pattern(filter_all, "*");
|
||||
gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter_all);
|
||||
|
||||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||||
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
|
||||
char *uri = g_strdup_printf("file://%s", filename);
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab && tab->webview) {
|
||||
webkit_web_view_load_uri(tab->webview, uri);
|
||||
}
|
||||
g_free(uri);
|
||||
g_free(filename);
|
||||
}
|
||||
gtk_widget_destroy(dialog);
|
||||
}
|
||||
|
||||
static void on_menu_history_item(GtkMenuItem *item, gpointer data) {
|
||||
(void)data;
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab == NULL || tab->webview == NULL || item == NULL) return;
|
||||
const char *url = gtk_menu_item_get_label(item);
|
||||
if (url && url[0]) {
|
||||
char *normalized = normalize_url(url);
|
||||
if (normalized) {
|
||||
webkit_web_view_load_uri(tab->webview, normalized);
|
||||
g_free(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void on_menu_history_clear(GtkMenuItem *item, gpointer data) {
|
||||
(void)item;
|
||||
(void)data;
|
||||
history_clear();
|
||||
g_print("[history] cleared\n");
|
||||
}
|
||||
|
||||
static void on_history_menu_show(GtkWidget *menu, gpointer user_data) {
|
||||
(void)user_data;
|
||||
|
||||
/* Remove all existing items. */
|
||||
GList *children = gtk_container_get_children(GTK_CONTAINER(menu));
|
||||
for (GList *l = children; l != NULL; l = l->next) {
|
||||
gtk_widget_destroy(GTK_WIDGET(l->data));
|
||||
}
|
||||
g_list_free(children);
|
||||
|
||||
/* Rebuild with current history. */
|
||||
int hcount = history_count();
|
||||
if (hcount == 0) {
|
||||
GtkWidget *empty = gtk_menu_item_new_with_label("(no recent pages)");
|
||||
gtk_widget_set_sensitive(empty, FALSE);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), empty);
|
||||
} else {
|
||||
int show = hcount < 20 ? hcount : 20;
|
||||
for (int i = 0; i < show; i++) {
|
||||
const char *url = history_get(i);
|
||||
if (url == NULL) break;
|
||||
char label[80];
|
||||
if (strlen(url) > 75) {
|
||||
snprintf(label, sizeof(label), "%.72s...", url);
|
||||
} else {
|
||||
snprintf(label, sizeof(label), "%s", url);
|
||||
}
|
||||
GtkWidget *hitem = gtk_menu_item_new_with_label(label);
|
||||
gtk_widget_set_tooltip_text(hitem, url);
|
||||
g_signal_connect(hitem, "activate",
|
||||
G_CALLBACK(on_menu_history_item), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), hitem);
|
||||
}
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
GtkWidget *clear_item = gtk_menu_item_new_with_label("Clear Recents");
|
||||
g_signal_connect(clear_item, "activate",
|
||||
G_CALLBACK(on_menu_history_clear), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), clear_item);
|
||||
}
|
||||
gtk_widget_show_all(menu);
|
||||
}
|
||||
|
||||
/* ── Hamburger menu builder ───────────────────────────────────────── */
|
||||
|
||||
static GtkWidget *build_hamburger_menu(tab_info_t *tab) {
|
||||
(void)tab;
|
||||
GtkWidget *menu = gtk_menu_new();
|
||||
|
||||
/* Navigation group. */
|
||||
GtkWidget *item_open = gtk_menu_item_new_with_label("Open File…");
|
||||
GtkWidget *item_reload = gtk_menu_item_new_with_label("Reload");
|
||||
GtkWidget *item_stop = gtk_menu_item_new_with_label("Stop");
|
||||
g_signal_connect(item_open, "activate", G_CALLBACK(on_menu_open_file), NULL);
|
||||
g_signal_connect(item_reload, "activate", G_CALLBACK(on_menu_reload), NULL);
|
||||
g_signal_connect(item_stop, "activate", G_CALLBACK(on_menu_stop), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_open);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_reload);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_stop);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Recents submenu. */
|
||||
GtkWidget *history_menu = gtk_menu_new();
|
||||
g_signal_connect(history_menu, "show", G_CALLBACK(on_history_menu_show), NULL);
|
||||
GtkWidget *item_history = gtk_menu_item_new_with_label("Recents");
|
||||
gtk_menu_item_set_submenu(GTK_MENU_ITEM(item_history), history_menu);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_history);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Identity group — delegated to main.c (app_state_t). */
|
||||
GtkWidget *item_switch = gtk_menu_item_new_with_label("Switch Identity…");
|
||||
GtkWidget *item_lock = gtk_menu_item_new_with_label("Lock Session");
|
||||
GtkWidget *item_logout = gtk_menu_item_new_with_label("Logout");
|
||||
g_signal_connect(item_switch, "activate",
|
||||
G_CALLBACK(app_menu_switch_identity_proxy), g_window);
|
||||
g_signal_connect(item_lock, "activate",
|
||||
G_CALLBACK(app_menu_lock_session_proxy), NULL);
|
||||
g_signal_connect(item_logout, "activate",
|
||||
G_CALLBACK(app_menu_logout_proxy), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_switch);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_lock);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_logout);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
/* Roadmap group. */
|
||||
GtkWidget *item_security =
|
||||
gtk_check_menu_item_new_with_label("Security strip (SOP/CORS/certs)");
|
||||
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item_security), FALSE);
|
||||
g_signal_connect(item_security, "toggled",
|
||||
G_CALLBACK(app_menu_security_strip_proxy), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_security);
|
||||
|
||||
GtkWidget *item_fips = gtk_menu_item_new_with_label("FIPS URI scheme…");
|
||||
GtkWidget *item_nostr = gtk_menu_item_new_with_label("Nostr signing status");
|
||||
g_signal_connect(item_fips, "activate",
|
||||
G_CALLBACK(app_menu_fips_proxy), NULL);
|
||||
g_signal_connect(item_nostr, "activate",
|
||||
G_CALLBACK(app_menu_nostr_sign_proxy), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_fips);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_nostr);
|
||||
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu),
|
||||
gtk_separator_menu_item_new());
|
||||
|
||||
GtkWidget *item_inspector = gtk_menu_item_new_with_label("Toggle Inspector");
|
||||
g_signal_connect(item_inspector, "activate",
|
||||
G_CALLBACK(on_menu_inspector), NULL);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_inspector);
|
||||
|
||||
GtkWidget *item_settings = gtk_menu_item_new_with_label("Settings…");
|
||||
g_signal_connect(item_settings, "activate",
|
||||
G_CALLBACK(on_menu_settings), g_window);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_settings);
|
||||
|
||||
GtkWidget *item_about = gtk_menu_item_new_with_label("About");
|
||||
g_signal_connect(item_about, "activate",
|
||||
G_CALLBACK(app_menu_about_proxy), g_window);
|
||||
gtk_menu_shell_append(GTK_MENU_SHELL(menu), item_about);
|
||||
|
||||
gtk_widget_show_all(menu);
|
||||
|
||||
GtkWidget *button = gtk_menu_button_new();
|
||||
gtk_menu_button_set_popup(GTK_MENU_BUTTON(button), menu);
|
||||
gtk_button_set_image(
|
||||
GTK_BUTTON(button),
|
||||
gtk_image_new_from_icon_name("open-menu-symbolic",
|
||||
GTK_ICON_SIZE_BUTTON));
|
||||
gtk_widget_set_tooltip_text(button, "Menu");
|
||||
return button;
|
||||
}
|
||||
|
||||
/* ── Tab array management ─────────────────────────────────────────── */
|
||||
|
||||
static int tab_array_add(tab_info_t *tab) {
|
||||
if (g_tab_count >= g_tab_cap) {
|
||||
int new_cap = g_tab_cap == 0 ? 8 : g_tab_cap * 2;
|
||||
tab_info_t **new_arr = g_realloc(g_tabs, new_cap * sizeof(tab_info_t *));
|
||||
if (new_arr == NULL) return -1;
|
||||
g_tabs = new_arr;
|
||||
g_tab_cap = new_cap;
|
||||
}
|
||||
g_tabs[g_tab_count] = tab;
|
||||
return g_tab_count++;
|
||||
}
|
||||
|
||||
static void tab_array_remove(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
/* Free the tab_info_t. */
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab) {
|
||||
/* The webview and widgets are destroyed by GtkNotebook when the
|
||||
* page is removed. We just free the struct. */
|
||||
g_free(tab);
|
||||
}
|
||||
/* Shift remaining entries down. */
|
||||
for (int i = index; i < g_tab_count - 1; i++) {
|
||||
g_tabs[i] = g_tabs[i + 1];
|
||||
}
|
||||
g_tab_count--;
|
||||
}
|
||||
|
||||
/* ── Tab creation ─────────────────────────────────────────────────── */
|
||||
|
||||
static tab_info_t *tab_create(const char *url) {
|
||||
const browser_settings_t *s = settings_get();
|
||||
|
||||
tab_info_t *tab = g_new0(tab_info_t, 1);
|
||||
if (tab == NULL) return NULL;
|
||||
|
||||
/* Determine the URL to load. */
|
||||
const char *load_url = url;
|
||||
char *default_url = NULL;
|
||||
if (load_url == NULL || load_url[0] == '\0') {
|
||||
load_url = s->new_tab_url;
|
||||
}
|
||||
default_url = normalize_url(load_url);
|
||||
if (default_url == NULL) {
|
||||
default_url = g_strdup(load_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));
|
||||
|
||||
/* Enable developer extras + security settings (same as original main.c). */
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(tab->webview);
|
||||
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);
|
||||
|
||||
/* Inject window.nostr into this webview. */
|
||||
nostr_inject_setup(tab->webview);
|
||||
|
||||
/* Build the per-tab page: toolbar on top, webview below. */
|
||||
tab->page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_margin_top(toolbar, 4);
|
||||
gtk_widget_set_margin_bottom(toolbar, 4);
|
||||
gtk_widget_set_margin_start(toolbar, 4);
|
||||
gtk_widget_set_margin_end(toolbar, 4);
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), toolbar, FALSE, FALSE, 0);
|
||||
|
||||
tab->hamburger = build_hamburger_menu(tab);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar), tab->hamburger, FALSE, FALSE, 0);
|
||||
|
||||
tab->url_entry = gtk_entry_new();
|
||||
gtk_entry_set_text(GTK_ENTRY(tab->url_entry), default_url);
|
||||
gtk_box_pack_start(GTK_BOX(toolbar), tab->url_entry, TRUE, TRUE, 0);
|
||||
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), GTK_WIDGET(tab->webview),
|
||||
TRUE, TRUE, 0);
|
||||
|
||||
/* Build the tab label. */
|
||||
tab->tab_label = build_tab_label(tab);
|
||||
|
||||
/* Wire signals. */
|
||||
g_signal_connect(tab->url_entry, "activate",
|
||||
G_CALLBACK(on_url_activate), tab);
|
||||
g_signal_connect(tab->webview, "load-changed",
|
||||
G_CALLBACK(on_load_changed), tab);
|
||||
g_signal_connect(tab->webview, "load-failed",
|
||||
G_CALLBACK(on_load_failed), NULL);
|
||||
|
||||
/* Load the URL. */
|
||||
webkit_web_view_load_uri(tab->webview, default_url);
|
||||
g_free(default_url);
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
/* ── New tab button ───────────────────────────────────────────────── */
|
||||
|
||||
static void on_new_tab_clicked(GtkButton *btn, gpointer data) {
|
||||
(void)btn;
|
||||
(void)data;
|
||||
tab_manager_new_tab(NULL);
|
||||
}
|
||||
|
||||
/* ── Public API ───────────────────────────────────────────────────── */
|
||||
|
||||
void tab_manager_init(GtkContainer *parent,
|
||||
WebKitWebContext *ctx,
|
||||
GtkWindow *window) {
|
||||
g_ctx = ctx;
|
||||
g_window = window;
|
||||
|
||||
g_notebook = gtk_notebook_new();
|
||||
gtk_notebook_set_scrollable(GTK_NOTEBOOK(g_notebook), TRUE);
|
||||
gtk_notebook_set_show_border(GTK_NOTEBOOK(g_notebook), FALSE);
|
||||
|
||||
/* New-tab button as an action widget at the end of the tab strip. */
|
||||
GtkWidget *new_btn = gtk_button_new();
|
||||
gtk_button_set_relief(GTK_BUTTON(new_btn), GTK_RELIEF_NONE);
|
||||
gtk_button_set_image(GTK_BUTTON(new_btn),
|
||||
gtk_image_new_from_icon_name("tab-new-symbolic",
|
||||
GTK_ICON_SIZE_BUTTON));
|
||||
gtk_widget_set_tooltip_text(new_btn, "New tab (Ctrl+T)");
|
||||
g_signal_connect(new_btn, "clicked", G_CALLBACK(on_new_tab_clicked), NULL);
|
||||
gtk_widget_show_all(new_btn);
|
||||
gtk_notebook_set_action_widget(GTK_NOTEBOOK(g_notebook), new_btn,
|
||||
GTK_PACK_END);
|
||||
|
||||
gtk_container_add(parent, g_notebook);
|
||||
|
||||
tab_manager_apply_settings();
|
||||
}
|
||||
|
||||
void tab_manager_apply_settings(void) {
|
||||
if (g_notebook == NULL) return;
|
||||
const browser_settings_t *s = settings_get();
|
||||
|
||||
gtk_notebook_set_tab_pos(GTK_NOTEBOOK(g_notebook), s->tab_bar_position);
|
||||
|
||||
/* Apply drag reorder to all existing tabs. */
|
||||
for (int i = 0; i < g_tab_count; i++) {
|
||||
if (g_tabs[i] && g_tabs[i]->page) {
|
||||
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(g_notebook),
|
||||
g_tabs[i]->page,
|
||||
s->tab_drag_reorder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int tab_manager_new_tab(const char *url) {
|
||||
const browser_settings_t *s = settings_get();
|
||||
if (g_tab_count >= s->max_tabs) {
|
||||
g_print("[tabs] Maximum tab count (%d) reached\n", s->max_tabs);
|
||||
return -1;
|
||||
}
|
||||
|
||||
tab_info_t *tab = tab_create(url);
|
||||
if (tab == NULL) return -1;
|
||||
|
||||
int index = tab_array_add(tab);
|
||||
if (index < 0) {
|
||||
g_free(tab);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Add to notebook. */
|
||||
int page_num = gtk_notebook_append_page(GTK_NOTEBOOK(g_notebook),
|
||||
tab->page, tab->tab_label);
|
||||
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(g_notebook), tab->page,
|
||||
s->tab_drag_reorder);
|
||||
|
||||
/* Switch to the new tab. */
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), page_num);
|
||||
|
||||
/* Show everything. */
|
||||
gtk_widget_show_all(tab->page);
|
||||
gtk_widget_show_all(tab->tab_label);
|
||||
|
||||
g_print("[tabs] Created tab %d, total: %d\n", page_num, g_tab_count);
|
||||
return page_num;
|
||||
}
|
||||
|
||||
void tab_manager_close_tab(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab == NULL) return;
|
||||
|
||||
/* Remove from notebook (this destroys the page widget). */
|
||||
gtk_notebook_remove_page(GTK_NOTEBOOK(g_notebook), 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 (g_tab_count == 0) {
|
||||
g_print("[tabs] Last tab closed, exiting.\n");
|
||||
if (g_window) {
|
||||
gtk_window_close(g_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tab_manager_close_active(void) {
|
||||
int index = tab_manager_get_active_index();
|
||||
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));
|
||||
}
|
||||
|
||||
void tab_manager_switch_to(int index) {
|
||||
if (g_notebook == NULL) return;
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), 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];
|
||||
}
|
||||
|
||||
tab_info_t *tab_manager_get(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return NULL;
|
||||
return g_tabs[index];
|
||||
}
|
||||
|
||||
int tab_manager_count(void) {
|
||||
return g_tab_count;
|
||||
}
|
||||
|
||||
void tab_manager_set_title(int index, const char *title) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab == NULL || tab->title_label == NULL) return;
|
||||
|
||||
snprintf(tab->title, sizeof(tab->title), "%s",
|
||||
(title && title[0]) ? title : "(Untitled)");
|
||||
gtk_label_set_text(GTK_LABEL(tab->title_label), tab->title);
|
||||
}
|
||||
|
||||
const char *tab_manager_get_url(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return NULL;
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab == NULL) return NULL;
|
||||
return tab->current_url;
|
||||
}
|
||||
|
||||
void tab_manager_next(void) {
|
||||
if (g_notebook == NULL || g_tab_count == 0) return;
|
||||
int cur = gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook));
|
||||
int next = (cur + 1) % g_tab_count;
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), next);
|
||||
}
|
||||
|
||||
void tab_manager_prev(void) {
|
||||
if (g_notebook == NULL || g_tab_count == 0) return;
|
||||
int cur = gtk_notebook_get_current_page(GTK_NOTEBOOK(g_notebook));
|
||||
int prev = (cur - 1 + g_tab_count) % g_tab_count;
|
||||
gtk_notebook_set_current_page(GTK_NOTEBOOK(g_notebook), prev);
|
||||
}
|
||||
|
||||
void tab_manager_close_others(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
|
||||
/* Close tabs to the right first (indices shift as we close). */
|
||||
tab_manager_close_to_right(index);
|
||||
|
||||
/* Close tabs to the left (close from the start so indices stay valid). */
|
||||
while (index > 0) {
|
||||
tab_manager_close_tab(0);
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
void tab_manager_close_to_right(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
/* Close from the end so indices don't shift. */
|
||||
while (g_tab_count > index + 1) {
|
||||
tab_manager_close_tab(g_tab_count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void tab_manager_duplicate(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab == NULL) return;
|
||||
tab_manager_new_tab(tab->current_url);
|
||||
}
|
||||
|
||||
void tab_manager_reload(int index) {
|
||||
if (index < 0 || index >= g_tab_count) return;
|
||||
tab_info_t *tab = g_tabs[index];
|
||||
if (tab && tab->webview) {
|
||||
webkit_web_view_reload_bypass_cache(tab->webview);
|
||||
}
|
||||
}
|
||||
142
src/tab_manager.h
Normal file
142
src/tab_manager.h
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* tab_manager.h — multi-tab management for sovereign_browser
|
||||
*
|
||||
* Wraps a GtkNotebook and manages per-tab state: each tab has its own
|
||||
* toolbar (hamburger menu + URL entry) and WebKitWebView. All webviews
|
||||
* share a single WebKitWebContext so the sovereign:// bridge, security
|
||||
* settings, and TLS policy apply to every tab.
|
||||
*/
|
||||
|
||||
#ifndef TAB_MANAGER_H
|
||||
#define TAB_MANAGER_H
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define TAB_TITLE_MAX 256
|
||||
#define TAB_URL_MAX 2048
|
||||
|
||||
typedef struct {
|
||||
WebKitWebView *webview;
|
||||
GtkWidget *url_entry;
|
||||
GtkWidget *hamburger;
|
||||
GtkWidget *page; /* the vertical box (toolbar + webview) */
|
||||
GtkWidget *tab_label; /* composite label widget for the tab strip */
|
||||
GtkWidget *title_label; /* child of tab_label */
|
||||
char current_url[TAB_URL_MAX];
|
||||
char title[TAB_TITLE_MAX];
|
||||
} tab_info_t;
|
||||
|
||||
/*
|
||||
* Initialize the tab manager. Creates a GtkNotebook, adds it to parent,
|
||||
* and stores the shared web context for creating new webviews.
|
||||
*
|
||||
* parent: the GtkContainer (typically a GtkBox) to add the notebook to
|
||||
* ctx: the shared WebKitWebContext for all tabs
|
||||
* window: the top-level GtkWindow (for menu dialogs, file choosers, etc.)
|
||||
*/
|
||||
void tab_manager_init(GtkContainer *parent,
|
||||
WebKitWebContext *ctx,
|
||||
GtkWindow *window);
|
||||
|
||||
/*
|
||||
* Create a new tab and load the given URL (or the default new-tab URL
|
||||
* if url is NULL). Returns the tab index, or -1 on failure (e.g. max
|
||||
* tabs reached).
|
||||
*/
|
||||
int tab_manager_new_tab(const char *url);
|
||||
|
||||
/*
|
||||
* Close the tab at the given index. If it's the last tab, the window
|
||||
* destroy handler will fire (caller is responsible for quitting).
|
||||
*/
|
||||
void tab_manager_close_tab(int index);
|
||||
|
||||
/*
|
||||
* Close the currently active tab.
|
||||
*/
|
||||
void tab_manager_close_active(void);
|
||||
|
||||
/*
|
||||
* Get the index of the currently active tab.
|
||||
*/
|
||||
int tab_manager_get_active_index(void);
|
||||
|
||||
/*
|
||||
* Switch to the tab at the given index.
|
||||
*/
|
||||
void tab_manager_switch_to(int index);
|
||||
|
||||
/*
|
||||
* Get the tab_info_t for the active tab, or NULL if no tabs exist.
|
||||
*/
|
||||
tab_info_t *tab_manager_get_active(void);
|
||||
|
||||
/*
|
||||
* Get the tab_info_t for the tab at the given index, or NULL.
|
||||
*/
|
||||
tab_info_t *tab_manager_get(int index);
|
||||
|
||||
/*
|
||||
* Get the number of open tabs.
|
||||
*/
|
||||
int tab_manager_count(void);
|
||||
|
||||
/*
|
||||
* Update the title of the tab at the given index. Called from the
|
||||
* load-changed signal handler when the page title becomes available.
|
||||
*/
|
||||
void tab_manager_set_title(int index, const char *title);
|
||||
|
||||
/*
|
||||
* Get the URL of the tab at the given index (for session save).
|
||||
* Returns NULL if index is out of range.
|
||||
*/
|
||||
const char *tab_manager_get_url(int index);
|
||||
|
||||
/*
|
||||
* Cycle to the next tab (wraps around). Used by Ctrl+Tab / Ctrl+PageDown.
|
||||
*/
|
||||
void tab_manager_next(void);
|
||||
|
||||
/*
|
||||
* Cycle to the previous tab (wraps around). Used by Ctrl+Shift+Tab /
|
||||
* Ctrl+PageUp.
|
||||
*/
|
||||
void tab_manager_prev(void);
|
||||
|
||||
/*
|
||||
* Close all tabs except the one at the given index.
|
||||
*/
|
||||
void tab_manager_close_others(int index);
|
||||
|
||||
/*
|
||||
* Close all tabs to the right of the given index.
|
||||
*/
|
||||
void tab_manager_close_to_right(int index);
|
||||
|
||||
/*
|
||||
* Duplicate the tab at the given index (open a new tab with the same URL).
|
||||
*/
|
||||
void tab_manager_duplicate(int index);
|
||||
|
||||
/*
|
||||
* Reload the tab at the given index.
|
||||
*/
|
||||
void tab_manager_reload(int index);
|
||||
|
||||
/*
|
||||
* Apply current settings (tab bar position, drag reorder, close buttons).
|
||||
* Call after settings_change() or at init.
|
||||
*/
|
||||
void tab_manager_apply_settings(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TAB_MANAGER_H */
|
||||
@@ -11,9 +11,9 @@
|
||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||
#define SOVEREIGN_BROWSER_VERSION_H
|
||||
|
||||
#define SB_VERSION "v0.0.3"
|
||||
#define SB_VERSION "v0.0.4"
|
||||
#define SB_VERSION_MAJOR 0
|
||||
#define SB_VERSION_MINOR 0
|
||||
#define SB_VERSION_PATCH 3
|
||||
#define SB_VERSION_PATCH 4
|
||||
|
||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||
|
||||
Reference in New Issue
Block a user