Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7b5eb72c | ||
|
|
2bc5ec7771 | ||
|
|
992b09357a | ||
|
|
24d07946b9 | ||
|
|
e0607fe045 |
+31
-5
@@ -40,7 +40,9 @@ cleanup_on_exit() {
|
||||
}
|
||||
trap cleanup_on_exit EXIT INT TERM
|
||||
register_cleanup() {
|
||||
[[ -n "$1" ]] && CLEANUP_FILES+=("$1")
|
||||
# Guard so an empty argument (e.g. a failed tarball path) does not cause
|
||||
# this function to return non-zero and abort the script under `set -e`.
|
||||
[[ -n "$1" ]] && CLEANUP_FILES+=("$1") || true
|
||||
}
|
||||
|
||||
# --- Config -------------------------------------------------------------
|
||||
@@ -280,7 +282,19 @@ build_release_binary() {
|
||||
create_source_tarball() {
|
||||
local tarball_name="${BIN_NAME}-${NEW_VERSION#v}.tar.gz"
|
||||
|
||||
if tar -czf "$tarball_name" \
|
||||
# Write the tarball to a temp path OUTSIDE the archived tree (.) so that
|
||||
# creating it does not change the directory while tar is reading it
|
||||
# (which makes tar emit "file changed as we read it" and exit 1, which
|
||||
# under `set -e` would abort the whole release flow). Move it into place
|
||||
# only after tar finishes.
|
||||
local tmp_tarball
|
||||
tmp_tarball=$(mktemp -t "${tarball_name}.XXXXXX") || return 1
|
||||
|
||||
# tar exits 1 for "file changed as we read it", which is benign here
|
||||
# because the only thing changing is the tarball we are writing (now in
|
||||
# /tmp). Accept exit codes 0 and 1 as success; anything else is fatal.
|
||||
set +e
|
||||
tar -czf "$tmp_tarball" \
|
||||
--exclude="$BIN_NAME" \
|
||||
--exclude='.git*' \
|
||||
--exclude='*.log' \
|
||||
@@ -290,11 +304,23 @@ create_source_tarball() {
|
||||
--exclude='tests/local-site/assets/*.m4a' \
|
||||
--exclude='tests/local-site/assets/*.webm' \
|
||||
--exclude='tests/local-site/assets/*.ogg' \
|
||||
. > /dev/null 2>&1; then
|
||||
echo "$tarball_name"
|
||||
else
|
||||
. > /dev/null 2>&1
|
||||
local tar_rc=$?
|
||||
set -e
|
||||
|
||||
if [[ $tar_rc -ne 0 && $tar_rc -ne 1 ]]; then
|
||||
rm -f "$tmp_tarball"
|
||||
print_error "tar failed with exit code $tar_rc"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv "$tmp_tarball" "$tarball_name" 2>/dev/null; then
|
||||
rm -f "$tmp_tarball"
|
||||
print_error "Failed to move tarball into place: $tarball_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$tarball_name"
|
||||
}
|
||||
|
||||
create_gitea_release() {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Plan: Keyboard Shortcut to Toggle All Menu Bars (per-window)
|
||||
|
||||
## Goal
|
||||
|
||||
Add a configurable keyboard shortcut that shows/hides all the browser's
|
||||
"menu bars" — the per-tab toolbars, bookmark bars, and the tab strip —
|
||||
**independently per window**. Pressing the shortcut in one window hides
|
||||
that window's chrome only; other windows are unaffected. This mirrors how
|
||||
the agent sidebar already works (per-window `sidebar_visible` on
|
||||
[`window_state_t`](src/tab_manager.c:87)).
|
||||
|
||||
In sovereign_browser there is no traditional `GtkMenuBar`; each tab's chrome
|
||||
is two widgets packed at the top of the tab page
|
||||
([`tab->page`](src/tab_manager.c:2966)):
|
||||
|
||||
1. **`#main-toolbar`** — the horizontal GtkBox holding the hamburger menu
|
||||
button, refresh/stop, back, forward, URL entry, and bookmark button
|
||||
(built in [`tab_create()`](src/tab_manager.c:2973)).
|
||||
2. **`#bookmark-bar`** — the bookmarks toolbar below the URL toolbar
|
||||
([`tab->bookmark_bar`](src/tab_manager.c:3089)).
|
||||
|
||||
Plus the tab strip itself, which is the `GtkNotebook` header, controlled
|
||||
via `gtk_notebook_set_show_tabs()`.
|
||||
|
||||
Hiding all three gives the webview the entire window (a focus / kiosk
|
||||
mode). Pressing the shortcut again restores them.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. New shortcut action
|
||||
|
||||
Add `SHORTCUT_TOGGLE_TOOLBARS` to the enum in [`shortcuts.h`](src/shortcuts.h:29)
|
||||
(before `SHORTCUT_COUNT`) and a matching entry in the `g_registry` table in
|
||||
[`shortcuts.c`](src/shortcuts.c:24):
|
||||
|
||||
| Field | Value |
|
||||
|---------|----------------------------------------------------|
|
||||
| id | `toggle_toolbars` |
|
||||
| label | `Toggle menu bars` |
|
||||
| desc | `Show or hide the tab strip, toolbars, and bookmark bars in the active window` |
|
||||
| dflt | `<Control><Shift>m` |
|
||||
|
||||
Default `<Control><Shift>m` ("m" for menu) is free and mnemonic. It is a
|
||||
browser-level shortcut (not consumed by WebKit for web content), so it
|
||||
reaches the window-level `on_key_press` handler reliably.
|
||||
|
||||
Because the settings page enumerates actions via
|
||||
[`sovereign://settings/config`](src/nostr_bridge.c:2994) (which loops over
|
||||
`SHORTCUT_COUNT` calling [`shortcuts_meta()`](src/shortcuts.c:232) /
|
||||
[`shortcuts_get()`](src/shortcuts.c:202)), the new action appears
|
||||
automatically in the Keyboard Shortcuts section of
|
||||
[`www/settings.html`](www/settings.html:74) with no JS changes. NIP-78 sync
|
||||
also picks it up for free via [`shortcuts_serialize()`](src/shortcuts.c:247).
|
||||
|
||||
### 2. Track toolbar widgets on each tab
|
||||
|
||||
The `#main-toolbar` GtkBox is currently a local variable in
|
||||
[`tab_create()`](src/tab_manager.c:2973) and is not stored on
|
||||
[`tab_info_t`](src/tab_manager.h:23). Add a field:
|
||||
|
||||
```c
|
||||
GtkWidget *toolbar; /* #main-toolbar (hamburger + nav + URL) */
|
||||
```
|
||||
|
||||
to `tab_info_t` in [`tab_manager.h`](src/tab_manager.h:23), and assign
|
||||
`tab->toolbar = toolbar;` in [`tab_create()`](src/tab_manager.c:2973).
|
||||
`tab->bookmark_bar` already exists.
|
||||
|
||||
### 3. Per-window visibility flag
|
||||
|
||||
Add a field to [`window_state_t`](src/tab_manager.c:87) in
|
||||
[`tab_manager.c`](src/tab_manager.c):
|
||||
|
||||
```c
|
||||
gboolean toolbars_visible; /* per-window menu-bar visibility */
|
||||
```
|
||||
|
||||
Initialize it to `TRUE` for `g_main_window` (in
|
||||
[`tab_manager_init`](src/tab_manager.c:3598), near where
|
||||
`sidebar_visible = FALSE` is set at line 3643) and for each aux window
|
||||
created in [`tab_manager_new_window`](src/tab_manager.c:1262) (near line
|
||||
1376 where `sidebar_visible = FALSE` is set).
|
||||
|
||||
### 4. Toggle function (active window only)
|
||||
|
||||
Add a public function in [`tab_manager.c`](src/tab_manager.c):
|
||||
|
||||
```c
|
||||
void tab_manager_toggle_toolbars(void);
|
||||
```
|
||||
|
||||
Implementation — mirrors [`tab_manager_toggle_sidebar()`](src/tab_manager.c:4143):
|
||||
|
||||
1. `window_state_t *ws = get_active_window_state();` — resolves the focused
|
||||
window (main or aux), exactly like the sidebar toggle does.
|
||||
2. Flip `ws->toolbars_visible`.
|
||||
3. `gtk_notebook_set_show_tabs(GTK_NOTEBOOK(ws->notebook), ws->toolbars_visible);`
|
||||
— hides/shows the tab strip for this window's notebook only.
|
||||
4. Iterate `g_tabs[0..g_tab_count)` and, for each tab whose **page belongs
|
||||
to this window's notebook** (use [`tab_find_notebook(tab->page)`](src/tab_manager.c:2660)
|
||||
and compare to `ws->notebook`), call
|
||||
`gtk_widget_set_visible(tab->toolbar, ws->toolbars_visible)` and
|
||||
`gtk_widget_set_visible(tab->bookmark_bar, ws->toolbars_visible)`.
|
||||
|
||||
This affects only the active window's tabs and tab strip; other windows
|
||||
keep their own `toolbars_visible` state.
|
||||
|
||||
### 5. Respect hidden state for new tabs
|
||||
|
||||
In [`tab_create()`](src/tab_manager.c:2973), after building `toolbar` and
|
||||
`tab->bookmark_bar`, look up the notebook the tab is being added to (the
|
||||
target notebook — `g_active_notebook` / `get_effective_notebook()` at tab
|
||||
creation time, or `tab_find_notebook(tab->page)` after packing) and its
|
||||
`window_state_t` via [`window_state_for_notebook()`](src/tab_manager.c:4051),
|
||||
then apply that window's flag:
|
||||
|
||||
```c
|
||||
window_state_t *ws = window_state_for_notebook(<target notebook>);
|
||||
gboolean vis = ws ? ws->toolbars_visible : TRUE;
|
||||
gtk_widget_set_visible(toolbar, vis);
|
||||
gtk_widget_set_visible(tab->bookmark_bar, vis);
|
||||
```
|
||||
|
||||
So a tab opened in a hidden-chrome window is also hidden, while a tab
|
||||
opened in a visible-chrome window stays visible. The tab strip visibility
|
||||
is per-notebook via `gtk_notebook_set_show_tabs()`, so no extra work is
|
||||
needed there.
|
||||
|
||||
### 6. Dispatch the shortcut
|
||||
|
||||
Add a case to the switch in [`on_key_press`](src/main.c:611) in
|
||||
[`main.c`](src/main.c):
|
||||
|
||||
```c
|
||||
case SHORTCUT_TOGGLE_TOOLBARS:
|
||||
tab_manager_toggle_toolbars();
|
||||
return TRUE;
|
||||
```
|
||||
|
||||
The key-press handler is window-level, so `get_active_window_state()`
|
||||
inside the toggle resolves to the window that had focus when the key was
|
||||
pressed — giving the per-window behavior.
|
||||
|
||||
### 7. Expose in tab_manager.h
|
||||
|
||||
Add the prototype for `tab_manager_toggle_toolbars(void)` near the other
|
||||
toggle prototypes ([`tab_manager_toggle_inspector`](src/tab_manager.h),
|
||||
[`tab_manager_toggle_sidebar`](src/tab_manager.h)).
|
||||
|
||||
## Files touched
|
||||
|
||||
| File | Change |
|
||||
|---------------------|---------------------------------------------------------------|
|
||||
| [`src/shortcuts.h`](src/shortcuts.h:29) | Add `SHORTCUT_TOGGLE_TOOLBARS` to enum |
|
||||
| [`src/shortcuts.c`](src/shortcuts.c:24) | Add registry entry (id/label/desc/dflt) |
|
||||
| [`src/tab_manager.h`](src/tab_manager.h:23) | Add `GtkWidget *toolbar` field + `tab_manager_toggle_toolbars` prototype |
|
||||
| [`src/tab_manager.c`](src/tab_manager.c:87) | Add `toolbars_visible` to `window_state_t`; init to TRUE for main + aux windows |
|
||||
| [`src/tab_manager.c`](src/tab_manager.c:2973) | Store `tab->toolbar`; apply window's `toolbars_visible` on creation; implement `tab_manager_toggle_toolbars()` (active window only: tab strip + that window's tabs' toolbars/bookmark bars) |
|
||||
| [`src/main.c`](src/main.c:611) | Add `case SHORTCUT_TOGGLE_TOOLBARS` dispatch |
|
||||
|
||||
No changes needed in `www/` (settings page auto-discovers the action) or in
|
||||
`settings_sync.c` (NIP-78 sync already serializes all actions).
|
||||
|
||||
## Verification
|
||||
|
||||
1. `make` — builds clean.
|
||||
2. `./browser.sh restart --login-method generate --url https://example.com`
|
||||
3. Press `Ctrl+Shift+M` → in the focused window, toolbars + bookmark bar +
|
||||
tab strip disappear; the webview fills the entire window. Press again →
|
||||
they reappear.
|
||||
4. Open a second window (`Ctrl+N`). Hide chrome in window A with
|
||||
`Ctrl+Shift+M`; window B's chrome stays visible. Hide B independently.
|
||||
5. With chrome hidden in a window, `Ctrl+T` opens a new tab in that window
|
||||
whose toolbars are also hidden; the tab strip stays hidden too.
|
||||
6. Open `sovereign://settings` → Keyboard Shortcuts section lists
|
||||
"Toggle menu bars" with `Ctrl+Shift+M`; rebind it and confirm the new
|
||||
binding works.
|
||||
+32
-12
@@ -12,10 +12,31 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ── Synchronous JS evaluation ─────────────────────────────────────── *
|
||||
* WebKitGTK's evaluate_javascript is async. We use a nested GMainLoop
|
||||
* on the default context to wait for the result. The key is to acquire
|
||||
* the context before creating the loop so that the WebKit IPC sources
|
||||
* are properly dispatched.
|
||||
* WebKitGTK's evaluate_javascript is async. We run a nested GMainLoop
|
||||
* on the default main context until the JS callback (or a timeout)
|
||||
* quits it.
|
||||
*
|
||||
* This function is routinely called from within a Soup server source
|
||||
* callback (the MCP HTTP handler) that is already dispatching on the
|
||||
* default main context. Running a nested GMainLoop on the same context
|
||||
* is the standard GTK modal pattern (gtk_dialog_run does the same) and
|
||||
* is safe: g_main_loop_run() manages context ownership internally for
|
||||
* the nested run, and the outer loop's dispatch is suspended until the
|
||||
* inner loop quits.
|
||||
*
|
||||
* The PREVIOUS code additionally called g_main_context_acquire()/
|
||||
* g_main_context_release() around the loop. That was the bug: the
|
||||
* default context is already owned by the outer dispatch, so acquire()
|
||||
* fails silently (owner_count is NOT incremented), yet release() then
|
||||
* decrements it unconditionally — producing
|
||||
* "g_main_context_release_unlocked: assertion 'context->owner_count > 0'"
|
||||
* and, combined with the recursive check, a segfault. The fix is simply
|
||||
* to drop the explicit acquire/release and let g_main_loop_run() handle
|
||||
* ownership. We must NOT use a g_main_context_iteration() pump instead,
|
||||
* because that would re-dispatch unrelated ready sources (other Soup
|
||||
* requests, WebKit load events, GTK UI events) re-entrantly while a
|
||||
* tool call is in progress, which can deadlock when an MCP tool runs
|
||||
* during page loading.
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
@@ -48,6 +69,7 @@ static gboolean on_js_timeout(gpointer user_data) {
|
||||
js_eval_ctx_t *ctx = (js_eval_ctx_t *)user_data;
|
||||
if (!ctx->done) {
|
||||
g_printerr("[agent] JS evaluation timed out\n");
|
||||
ctx->done = TRUE;
|
||||
if (ctx->loop && g_main_loop_is_running(ctx->loop)) {
|
||||
g_main_loop_quit(ctx->loop);
|
||||
}
|
||||
@@ -61,26 +83,24 @@ char *agent_js_eval_sync(WebKitWebView *webview, const char *script,
|
||||
|
||||
js_eval_ctx_t ctx = {0};
|
||||
|
||||
/* Use the default main context for the nested loop. */
|
||||
GMainContext *main_ctx = g_main_context_default();
|
||||
g_main_context_acquire(main_ctx);
|
||||
|
||||
ctx.loop = g_main_loop_new(main_ctx, FALSE);
|
||||
/* Nested loop on the default context. Do NOT acquire/release the
|
||||
* context here — see the block comment above. */
|
||||
ctx.loop = g_main_loop_new(g_main_context_default(), FALSE);
|
||||
|
||||
/* Add a timeout source to the same context. */
|
||||
guint timeout_id = g_timeout_add(timeout_ms, on_js_timeout, &ctx);
|
||||
|
||||
/* Start async evaluation. */
|
||||
/* Start async evaluation. The completion callback fires on the
|
||||
* default main context (where WebKit schedules its async work). */
|
||||
webkit_web_view_evaluate_javascript(webview, script, -1, NULL, NULL, NULL,
|
||||
on_js_finished, &ctx);
|
||||
|
||||
/* Run the nested loop until JS completes or timeout. */
|
||||
/* Run the nested loop until JS completes or the timeout quits it. */
|
||||
g_main_loop_run(ctx.loop);
|
||||
|
||||
/* Cleanup. */
|
||||
g_source_remove(timeout_id);
|
||||
g_main_loop_unref(ctx.loop);
|
||||
g_main_context_release(main_ctx);
|
||||
|
||||
return ctx.result;
|
||||
}
|
||||
|
||||
+16
-12
@@ -1005,8 +1005,16 @@ static cJSON *tool_eval(cJSON *params) {
|
||||
/* ── Screenshot tool ──────────────────────────────────────────────── */
|
||||
|
||||
/* Context for the async webkit_web_view_snapshot() call. We use a
|
||||
* nested GMainLoop to turn the async API into a synchronous one,
|
||||
* matching the pattern in agent_js_eval_sync(). */
|
||||
* async API into a synchronous one.
|
||||
*
|
||||
* We run a nested GMainLoop on the default main context (the standard
|
||||
* GTK modal pattern) but do NOT call g_main_context_acquire()/
|
||||
* g_main_context_release() around it — the default context is already
|
||||
* owned by the outer dispatch (these tools are invoked from the MCP
|
||||
* HTTP handler), so an explicit acquire fails silently and a matching
|
||||
* release corrupts the owner_count, causing a segfault. The nested
|
||||
* g_main_loop_run() handles ownership internally. See the block comment
|
||||
* in agent_js_eval_sync() for the full rationale. */
|
||||
typedef struct {
|
||||
cairo_surface_t *surface;
|
||||
gboolean done;
|
||||
@@ -1034,6 +1042,7 @@ static gboolean snapshot_timeout_cb(gpointer user_data) {
|
||||
snapshot_ctx_t *ctx = (snapshot_ctx_t *)user_data;
|
||||
if (!ctx->done) {
|
||||
g_printerr("[agent] screenshot timed out\n");
|
||||
ctx->done = TRUE;
|
||||
if (ctx->loop && g_main_loop_is_running(ctx->loop)) {
|
||||
g_main_loop_quit(ctx->loop);
|
||||
}
|
||||
@@ -1054,11 +1063,10 @@ static cJSON *tool_screenshot(cJSON *params) {
|
||||
WebKitWebView *wv = get_active_webview();
|
||||
if (!wv) return make_error("NO_TAB", "No active tab");
|
||||
|
||||
/* Set up the nested GMainLoop to wait for the async snapshot. */
|
||||
/* Nested GMainLoop on the default context. No acquire/release —
|
||||
* see the note on snapshot_ctx_t above. */
|
||||
snapshot_ctx_t ctx = {0};
|
||||
GMainContext *main_ctx = g_main_context_default();
|
||||
g_main_context_acquire(main_ctx);
|
||||
ctx.loop = g_main_loop_new(main_ctx, FALSE);
|
||||
ctx.loop = g_main_loop_new(g_main_context_default(), FALSE);
|
||||
|
||||
guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx);
|
||||
|
||||
@@ -1074,7 +1082,6 @@ static cJSON *tool_screenshot(cJSON *params) {
|
||||
|
||||
g_source_remove(timeout_id);
|
||||
g_main_loop_unref(ctx.loop);
|
||||
g_main_context_release(main_ctx);
|
||||
|
||||
if (!ctx.done || ctx.surface == NULL) {
|
||||
if (ctx.surface) cairo_surface_destroy(ctx.surface);
|
||||
@@ -4036,11 +4043,9 @@ static cJSON *tool_screenshot_annotated(cJSON *params) {
|
||||
/* We don't strictly need the result, but wait for it to complete. */
|
||||
g_free(ann_result);
|
||||
|
||||
/* Step 3: Take the WebKit snapshot (same pattern as tool_screenshot). */
|
||||
/* Step 3: Take the WebKit snapshot (same nested-loop pattern as tool_screenshot). */
|
||||
snapshot_ctx_t ctx = {0};
|
||||
GMainContext *main_ctx = g_main_context_default();
|
||||
g_main_context_acquire(main_ctx);
|
||||
ctx.loop = g_main_loop_new(main_ctx, FALSE);
|
||||
ctx.loop = g_main_loop_new(g_main_context_default(), FALSE);
|
||||
|
||||
guint timeout_id = g_timeout_add(10000, snapshot_timeout_cb, &ctx);
|
||||
|
||||
@@ -4053,7 +4058,6 @@ static cJSON *tool_screenshot_annotated(cJSON *params) {
|
||||
|
||||
g_source_remove(timeout_id);
|
||||
g_main_loop_unref(ctx.loop);
|
||||
g_main_context_release(main_ctx);
|
||||
|
||||
if (!ctx.done || ctx.surface == NULL) {
|
||||
if (ctx.surface) cairo_surface_destroy(ctx.surface);
|
||||
|
||||
@@ -627,6 +627,10 @@ gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
|
||||
tab_manager_toggle_sidebar();
|
||||
return TRUE;
|
||||
|
||||
case SHORTCUT_TOGGLE_TOOLBARS:
|
||||
tab_manager_toggle_toolbars();
|
||||
return TRUE;
|
||||
|
||||
case SHORTCUT_ZOOM_IN:
|
||||
tab_manager_zoom_in();
|
||||
return TRUE;
|
||||
|
||||
@@ -100,6 +100,11 @@ static const shortcut_meta_t g_registry[SHORTCUT_COUNT] = {
|
||||
"toggle_sidebar", "Toggle agent sidebar",
|
||||
"Show or hide the agent chat sidebar", "<Control><Shift>a"
|
||||
},
|
||||
[SHORTCUT_TOGGLE_TOOLBARS] = {
|
||||
"toggle_toolbars", "Toggle menu bars",
|
||||
"Show or hide the tab strip, toolbars, and bookmark bars in the active window",
|
||||
"<Control><Shift>m"
|
||||
},
|
||||
[SHORTCUT_ZOOM_IN] = {
|
||||
"zoom_in", "Zoom in",
|
||||
"Increase the zoom level of the active tab", "<Control>plus"
|
||||
|
||||
@@ -46,6 +46,7 @@ typedef enum {
|
||||
SHORTCUT_TOGGLE_FULLSCREEN,
|
||||
SHORTCUT_TOGGLE_INSPECTOR,
|
||||
SHORTCUT_TOGGLE_SIDEBAR,
|
||||
SHORTCUT_TOGGLE_TOOLBARS,
|
||||
SHORTCUT_ZOOM_IN,
|
||||
SHORTCUT_ZOOM_OUT,
|
||||
SHORTCUT_ZOOM_RESET,
|
||||
|
||||
@@ -91,6 +91,8 @@ typedef struct {
|
||||
GtkWidget *sidebar_container; /* GtkBox for sidebar webview */
|
||||
WebKitWebView *sidebar_webview; /* lazily created on first toggle */
|
||||
gboolean sidebar_visible;
|
||||
gboolean toolbars_visible; /* per-window menu-bar (tab strip +
|
||||
* toolbars + bookmark bars) visibility */
|
||||
} window_state_t;
|
||||
|
||||
/* ── Static state ─────────────────────────────────────────────────── */
|
||||
@@ -1374,6 +1376,7 @@ static GtkWidget *tab_manager_new_window(const char *url,
|
||||
ws.sidebar_container = sidebar_container;
|
||||
ws.sidebar_webview = NULL;
|
||||
ws.sidebar_visible = FALSE;
|
||||
ws.toolbars_visible = TRUE;
|
||||
g_array_append_val(g_aux_windows, ws);
|
||||
|
||||
/* This new window is now the active window. */
|
||||
@@ -2976,6 +2979,7 @@ static tab_info_t *tab_create(const char *url) {
|
||||
gtk_widget_set_margin_bottom(toolbar, 4);
|
||||
gtk_widget_set_margin_start(toolbar, 4);
|
||||
gtk_widget_set_margin_end(toolbar, 4);
|
||||
tab->toolbar = toolbar;
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), toolbar, FALSE, FALSE, 0);
|
||||
|
||||
tab->hamburger = build_hamburger_menu(tab);
|
||||
@@ -3095,6 +3099,17 @@ static tab_info_t *tab_create(const char *url) {
|
||||
gtk_box_pack_start(GTK_BOX(tab->page), tab->bookmark_bar, FALSE, FALSE, 0);
|
||||
bookmark_bar_refresh(tab);
|
||||
|
||||
/* Apply the active window's menu-bar visibility so a tab opened in a
|
||||
* hidden-chrome window is also hidden. The tab strip itself is
|
||||
* per-notebook (gtk_notebook_set_show_tabs), already set by the
|
||||
* toggle; here we only sync the per-tab toolbar + bookmark bar. */
|
||||
{
|
||||
window_state_t *ws = get_active_window_state();
|
||||
gboolean vis = ws ? ws->toolbars_visible : TRUE;
|
||||
gtk_widget_set_visible(tab->toolbar, vis);
|
||||
gtk_widget_set_visible(tab->bookmark_bar, vis);
|
||||
}
|
||||
|
||||
/* Ensure the webview expands vertically to fill the available space.
|
||||
* Without this, WebKitGTK may only allocate 1px of height on some
|
||||
* display servers, resulting in a blank page. */
|
||||
@@ -3641,6 +3656,7 @@ void tab_manager_init(GtkContainer *parent,
|
||||
g_main_window.notebook = g_notebook;
|
||||
g_main_window.sidebar_webview = NULL;
|
||||
g_main_window.sidebar_visible = FALSE;
|
||||
g_main_window.toolbars_visible = TRUE;
|
||||
|
||||
/* The main window/notebook are the default active window/notebook
|
||||
* for MCP get_active_webview() and keyboard shortcuts (zoom, etc.).
|
||||
@@ -4164,6 +4180,28 @@ void tab_manager_toggle_sidebar(void) {
|
||||
}
|
||||
}
|
||||
|
||||
void tab_manager_toggle_toolbars(void) {
|
||||
window_state_t *ws = get_active_window_state();
|
||||
if (ws == NULL || ws->notebook == NULL) return;
|
||||
|
||||
ws->toolbars_visible = !ws->toolbars_visible;
|
||||
gboolean vis = ws->toolbars_visible;
|
||||
|
||||
/* Tab strip (GtkNotebook header) — per-notebook. */
|
||||
gtk_notebook_set_show_tabs(GTK_NOTEBOOK(ws->notebook), vis);
|
||||
|
||||
/* Per-tab toolbar + bookmark bar, but only for tabs that belong to
|
||||
* this window's notebook (aux windows have their own notebook). */
|
||||
for (int i = 0; i < g_tab_count; i++) {
|
||||
tab_info_t *tab = g_tabs[i];
|
||||
if (tab == NULL || tab->page == NULL) continue;
|
||||
GtkWidget *nb = tab_find_notebook(tab->page);
|
||||
if (nb != ws->notebook) continue;
|
||||
if (tab->toolbar) gtk_widget_set_visible(tab->toolbar, vis);
|
||||
if (tab->bookmark_bar) gtk_widget_set_visible(tab->bookmark_bar, vis);
|
||||
}
|
||||
}
|
||||
|
||||
WebKitWebView *tab_manager_get_main_webview(void) {
|
||||
tab_info_t *tab = tab_manager_get_active();
|
||||
if (tab == NULL) return NULL;
|
||||
|
||||
@@ -29,6 +29,7 @@ typedef struct {
|
||||
GtkWidget *title_label; /* child of tab_label */
|
||||
GtkWidget *favicon; /* favicon image in tab_label */
|
||||
GtkWidget *refresh_btn; /* toolbar reload/stop button (per-tab) */
|
||||
GtkWidget *toolbar; /* #main-toolbar (hamburger + nav + URL) */
|
||||
GtkWidget *bookmark_bar; /* bookmarks toolbar (below URL toolbar) */
|
||||
gboolean refresh_shows_stop; /* cached visual state, avoids redundant updates */
|
||||
char current_url[TAB_URL_MAX];
|
||||
@@ -228,6 +229,15 @@ void tab_manager_zoom_reset(void);
|
||||
*/
|
||||
void tab_manager_toggle_sidebar(void);
|
||||
|
||||
/*
|
||||
* Toggle visibility of all "menu bars" in the active window: the tab strip
|
||||
* (GtkNotebook header), each tab's #main-toolbar, and each tab's bookmark
|
||||
* bar. Per-window — only the focused window is affected, mirroring
|
||||
* tab_manager_toggle_sidebar(). New tabs opened in a hidden-chrome window
|
||||
* inherit the hidden state.
|
||||
*/
|
||||
void tab_manager_toggle_toolbars(void);
|
||||
|
||||
/*
|
||||
* Returns the main webview (the web page) of the active tab, never the
|
||||
* sidebar webview. This is used by agent tools so they always operate
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@
|
||||
#ifndef SOVEREIGN_BROWSER_VERSION_H
|
||||
#define SOVEREIGN_BROWSER_VERSION_H
|
||||
|
||||
#define SB_VERSION "v0.0.58"
|
||||
#define SB_VERSION "v0.0.63"
|
||||
#define SB_VERSION_MAJOR 0
|
||||
#define SB_VERSION_MINOR 0
|
||||
#define SB_VERSION_PATCH 58
|
||||
#define SB_VERSION_PATCH 63
|
||||
|
||||
#endif /* SOVEREIGN_BROWSER_VERSION_H */
|
||||
|
||||
+184
-1
@@ -9,6 +9,7 @@
|
||||
#include "profile.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <webkit2/webkit2.h>
|
||||
|
||||
#include <string.h>
|
||||
@@ -18,6 +19,173 @@
|
||||
* wants the per-user context. */
|
||||
static WebKitWebContext *g_ctx = NULL;
|
||||
|
||||
/* ── Download handling ─────────────────────────────────────────────── *
|
||||
* WebKitGTK fires the "download-started" signal on the WebKitWebContext
|
||||
* whenever the user triggers a download — most notably the "Save Image"
|
||||
* context-menu item on an <img>. In the webkit2gtk-4.1 API the download
|
||||
* does NOT auto-prompt for a destination: the application must handle the
|
||||
* download's "decide-destination" signal (which supplies a suggested
|
||||
* filename), call webkit_download_set_destination(), and return TRUE.
|
||||
* If nobody handles "decide-destination", WebKit writes the file to
|
||||
* G_USER_DIRECTORY_DOWNLOAD using the suggested name — but on many
|
||||
* minimal/containerized setups that directory resolves to a path the
|
||||
* user never sees, so "Save Image" appears to do nothing.
|
||||
*
|
||||
* Our flow:
|
||||
* download-started (context) → take a ref, wire finished/failed
|
||||
* decide-destination (download) → show a GtkFileChooserDialog pre-filled
|
||||
* with the suggested filename, set the
|
||||
* chosen destination, return TRUE
|
||||
* finished / failed → unref the download
|
||||
*
|
||||
* Since 2.40, returning TRUE from "decide-destination" without
|
||||
* immediately calling webkit_download_set_destination() is allowed: the
|
||||
* download pauses until the destination is set or the download is
|
||||
* cancelled. We use that to run a modal file chooser inside the handler.
|
||||
*/
|
||||
|
||||
/* Find a reasonable parent GtkWindow for the file chooser. We prefer the
|
||||
* currently-active (focused) top-level window; if none is focused we fall
|
||||
* back to the first visible top-level. web_context.c deliberately does
|
||||
* not depend on tab_manager.c's g_active_window, so this stays self-
|
||||
* contained. Returns NULL if no usable window is found. */
|
||||
static GtkWindow *pick_dialog_parent(void) {
|
||||
GList *toplevels = gtk_window_list_toplevels();
|
||||
GtkWindow *active = NULL;
|
||||
GtkWindow *first_visible = NULL;
|
||||
|
||||
for (GList *l = toplevels; l != NULL; l = l->next) {
|
||||
GtkWindow *w = GTK_WINDOW(l->data);
|
||||
if (!gtk_widget_get_visible(GTK_WIDGET(w))) continue;
|
||||
if (first_visible == NULL) first_visible = w;
|
||||
if (gtk_window_is_active(w)) { active = w; break; }
|
||||
}
|
||||
|
||||
GtkWindow *result = active ? active : first_visible;
|
||||
if (result != NULL) g_object_ref_sink(G_OBJECT(result));
|
||||
g_list_free(toplevels);
|
||||
return result; /* caller unrefs */
|
||||
}
|
||||
|
||||
/* Reduce a possibly-full suggested path to its final path segment so the
|
||||
* save dialog never starts in an unexpected directory. Returns a newly
|
||||
* allocated string. */
|
||||
static char *basename_of(const char *name) {
|
||||
if (name == NULL || name[0] == '\0') return NULL;
|
||||
const char *slash = strrchr(name, '/');
|
||||
return g_strdup(slash ? slash + 1 : name);
|
||||
}
|
||||
|
||||
static void on_download_finished(WebKitDownload *dl, gpointer user) {
|
||||
(void)user;
|
||||
const char *dest = webkit_download_get_destination(dl);
|
||||
g_print("[download] Finished: %s\n", dest ? dest : "(no destination)");
|
||||
g_object_unref(dl);
|
||||
}
|
||||
|
||||
static void on_download_failed(WebKitDownload *dl, GError *err, gpointer user) {
|
||||
(void)user;
|
||||
g_printerr("[download] Failed: %s\n", err ? err->message : "(unknown)");
|
||||
g_object_unref(dl);
|
||||
}
|
||||
|
||||
/* "decide-destination" handler. WebKit passes the suggested filename
|
||||
* (derived from the response Content-Disposition / URL). We show a save
|
||||
* dialog, set the destination, and return TRUE. Returning TRUE without
|
||||
* setting a destination is explicitly supported since 2.40 and pauses
|
||||
* the download until we set one (or cancel). */
|
||||
|
||||
/* Remember the directory the user last saved a download to, so the next
|
||||
* save dialog opens in the same folder instead of the default. Owned by
|
||||
* this module (g_free'd on teardown). NULL until the first successful
|
||||
* save. */
|
||||
static char *g_last_save_dir = NULL;
|
||||
|
||||
static gboolean on_download_decide_destination(WebKitDownload *dl,
|
||||
const char *suggested,
|
||||
gpointer user) {
|
||||
(void)user;
|
||||
|
||||
char *default_name = basename_of(suggested);
|
||||
if (default_name == NULL || default_name[0] == '\0') {
|
||||
g_free(default_name);
|
||||
default_name = g_strdup("download");
|
||||
}
|
||||
|
||||
GtkWindow *parent = pick_dialog_parent();
|
||||
|
||||
GtkWidget *dialog = gtk_file_chooser_dialog_new(
|
||||
"Save File", parent, GTK_FILE_CHOOSER_ACTION_SAVE,
|
||||
"_Cancel", GTK_RESPONSE_CANCEL,
|
||||
"_Save", GTK_RESPONSE_ACCEPT,
|
||||
NULL);
|
||||
if (parent) g_object_unref(parent);
|
||||
|
||||
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
|
||||
|
||||
/* If the user saved a download before, open the dialog in that same
|
||||
* directory. Otherwise GTK falls back to the current working
|
||||
* directory / last-used folder for the application. */
|
||||
if (g_last_save_dir != NULL && g_last_save_dir[0] != '\0') {
|
||||
gtk_file_chooser_set_current_folder(chooser, g_last_save_dir);
|
||||
}
|
||||
|
||||
gtk_file_chooser_set_current_name(chooser, default_name);
|
||||
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
|
||||
|
||||
gint resp = gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
char *path = (resp == GTK_RESPONSE_ACCEPT)
|
||||
? gtk_file_chooser_get_filename(chooser)
|
||||
: NULL;
|
||||
gtk_widget_destroy(dialog);
|
||||
g_free(default_name);
|
||||
|
||||
if (path == NULL) {
|
||||
/* User cancelled — abort the download. */
|
||||
webkit_download_cancel(dl);
|
||||
return TRUE; /* we handled it (by cancelling) */
|
||||
}
|
||||
|
||||
/* Remember the directory the user chose for next time. */
|
||||
char *dir = g_path_get_dirname(path);
|
||||
if (dir != NULL && dir[0] != '\0' &&
|
||||
(g_last_save_dir == NULL ||
|
||||
g_strcmp0(dir, g_last_save_dir) != 0)) {
|
||||
g_free(g_last_save_dir);
|
||||
g_last_save_dir = g_strdup(dir);
|
||||
}
|
||||
g_free(dir);
|
||||
|
||||
char *uri = g_filename_to_uri(path, NULL, NULL);
|
||||
g_free(path);
|
||||
if (uri == NULL) {
|
||||
webkit_download_cancel(dl);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
webkit_download_set_destination(dl, uri);
|
||||
g_free(uri);
|
||||
return TRUE; /* stop other handlers; destination is now set */
|
||||
}
|
||||
|
||||
static void on_download_started(WebKitWebContext *ctx,
|
||||
WebKitDownload *dl,
|
||||
gpointer user_data) {
|
||||
(void)ctx;
|
||||
(void)user_data;
|
||||
|
||||
/* Hold a ref for the lifetime of the download so our finished/failed
|
||||
* handlers can safely access it; we unref there. */
|
||||
g_object_ref(dl);
|
||||
|
||||
g_signal_connect(dl, "decide-destination",
|
||||
G_CALLBACK(on_download_decide_destination), NULL);
|
||||
g_signal_connect(dl, "finished",
|
||||
G_CALLBACK(on_download_finished), NULL);
|
||||
g_signal_connect(dl, "failed",
|
||||
G_CALLBACK(on_download_failed), NULL);
|
||||
}
|
||||
|
||||
/* ── Security / TLS / favicon configuration ────────────────────────── *
|
||||
* Applied to every context we build (per-user and ephemeral). Mirrors
|
||||
* the configuration that main.c used to apply to the default context.
|
||||
@@ -27,12 +195,21 @@ static void configure_context(WebKitWebContext *ctx) {
|
||||
|
||||
/* Security strip: register sovereign://, tor://, file:// as secure
|
||||
* (no CORS enforcement), and file:// as local. Same as the original
|
||||
* main.c configuration. */
|
||||
* main.c configuration.
|
||||
*
|
||||
* Also register ws (insecure WebSocket) as secure. Without this,
|
||||
* WebKitGTK blocks ws:// connections opened from https:// pages as
|
||||
* mixed active content (a secure page may not connect to an
|
||||
* insecure origin). This browser is intentionally open and should
|
||||
* be able to connect to anywhere, including local plaintext
|
||||
* WebSocket services, so we treat ws:// as a secure scheme — the
|
||||
* same status wss:// already has by default. */
|
||||
WebKitSecurityManager *sec_mgr =
|
||||
webkit_web_context_get_security_manager(ctx);
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "sovereign");
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "tor");
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "file");
|
||||
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "ws");
|
||||
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "file");
|
||||
|
||||
/* Accept any TLS certificate (FIPS uses Noise IK, not TLS CAs). */
|
||||
@@ -58,6 +235,12 @@ static void configure_context(WebKitWebContext *ctx) {
|
||||
g_get_tmp_dir());
|
||||
}
|
||||
webkit_web_context_set_favicon_database_directory(ctx, fav_dir);
|
||||
|
||||
/* Handle downloads — most importantly the "Save Image" context-menu
|
||||
* action. Without this handler WebKitGTK cancels the download (and
|
||||
* the user sees nothing happen). See on_download_started() above. */
|
||||
g_signal_connect(ctx, "download-started",
|
||||
G_CALLBACK(on_download_started), NULL);
|
||||
}
|
||||
|
||||
WebKitWebContext *web_context_build_for_pubkey(const char *pubkey_hex) {
|
||||
|
||||
Reference in New Issue
Block a user