From d131762ce8b351591d736f481676ffbc39835160 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Thu, 17 Sep 2026 14:48:04 -0400 Subject: [PATCH] v0.0.6 - Fix Ctrl+Shift+M toolbar toggle (uppercase keyval) and add Toggle Toolbars to hamburger and page context menus --- Cargo.lock | 5 +- Cargo.toml | 5 +- VERSION | 2 +- increment_and_push.sh | 2 +- install_local.sh | 118 ++++++++++++++++++++ run.sh | 4 +- src/key_handler.rs | 245 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 216 ++----------------------------------- src/nostr_scheme.rs | 30 +++--- src/tab_manager.rs | 86 +++++++++++++-- src/version.rs | 4 +- 12 files changed, 477 insertions(+), 241 deletions(-) create mode 100755 install_local.sh create mode 100644 src/key_handler.rs diff --git a/Cargo.lock b/Cargo.lock index 2f38e04..19aad26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3055,8 +3055,8 @@ dependencies = [ ] [[package]] -name = "sovereign-browser" -version = "0.0.4" +name = "sovereign_browser" +version = "0.0.5" dependencies = [ "anyhow", "base64", @@ -3064,6 +3064,7 @@ dependencies = [ "cairo-rs", "chrono", "env_logger", + "futures-channel", "futures-util", "gdk", "gdk-pixbuf", diff --git a/Cargo.toml b/Cargo.toml index 8b7e11d..63ad653 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,8 @@ members = [ ] [package] -name = "sovereign-browser" -version = "0.0.5" +name = "sovereign_browser" +version = "0.0.6" edition = "2021" license = "MIT" description = "A Linux x86 web browser built on WebKitGTK with Nostr identity" @@ -32,6 +32,7 @@ pango = "0.18" # ── Async Runtime ─────────────────────────────────────────────────── tokio = { version = "1", features = ["full"] } futures-util = "0.3" +futures-channel = "0.3" # ── Serialization ─────────────────────────────────────────────────── serde = { version = "1", features = ["derive"] } diff --git a/VERSION b/VERSION index bbdeab6..1750564 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5 +0.0.6 diff --git a/increment_and_push.sh b/increment_and_push.sh index 74b366e..2cd6d8f 100755 --- a/increment_and_push.sh +++ b/increment_and_push.sh @@ -45,7 +45,7 @@ register_cleanup() { # Gitea repo for release uploads (derived from the origin remote). GITEA_API_URL="https://git.laantungir.net/api/v1/repos/laantungir/sovereign_browser_rust" -BIN_NAME="sovereign-browser" +BIN_NAME="sovereign_browser" RELEASE_BIN_PATH="target/release/${BIN_NAME}" # --- Args --------------------------------------------------------------- diff --git a/install_local.sh b/install_local.sh new file mode 100755 index 0000000..b1c7c09 --- /dev/null +++ b/install_local.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# install_local.sh — Build sovereign_browser from local source and install to +# /usr/local/bin (or $INSTALL_PREFIX/bin). +# +# This is a lightweight alternative to install.sh for developers who already +# have the build environment set up. It does NOT install apt dependencies, +# FIPS, or Tor — it just runs `cargo build --release` and copies the binary. +# +# Usage: +# ./install_local.sh # build + install to /usr/local +# INSTALL_PREFIX=/opt ./install_local.sh # install elsewhere +# ./install_local.sh --prefix /opt # same +# ./install_local.sh --help # show help + +set -euo pipefail + +INSTALL_PREFIX="${INSTALL_PREFIX:-/usr/local}" + +# --- Output helpers ------------------------------------------------------ + +if [[ -t 2 && -z "${NO_COLOR:-}" ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[1;33m' + BLUE='\033[0;34m' + NC='\033[0m' +else + RED='' GREEN='' YELLOW='' BLUE='' NC='' +fi + +print_info() { echo -e "${BLUE}[INFO]${NC} $*" >&2; } +print_success() { echo -e "${GREEN}[SUCCESS]${NC} $*" >&2; } +print_warning() { echo -e "${YELLOW}[WARNING]${NC} $*" >&2; } +print_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +die() { print_error "$*"; exit 1; } + +# --- Privilege helper ---------------------------------------------------- +# Returns "sudo" if the target path is not user-writable, else "". +sudo_for() { + local target="$1" + if [[ -w "$target" ]]; then + echo "" + else + command -v sudo >/dev/null 2>&1 || die "sudo is required to write to $target but is not available." + echo "sudo" + fi +} + +# --- Usage --------------------------------------------------------------- + +show_usage() { + cat <<'EOF' +install_local.sh — Build sovereign_browser from local source and install + +Usage: ./install_local.sh [options] + +Options: + --prefix Install prefix (default: /usr/local) + -h, --help Show this help + +Environment: + INSTALL_PREFIX Alternative to --prefix (default: /usr/local) +EOF +} + +# --- Args ---------------------------------------------------------------- + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) INSTALL_PREFIX="${2:-}"; shift 2 ;; + -h|--help) show_usage; exit 0 ;; + *) die "Unknown option: $1 (try --help)" ;; + esac +done + +# --- Config -------------------------------------------------------------- +# Resolve the project root as the directory containing this script. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +BIN_NAME="sovereign_browser" +BUILT_BIN="target/release/${BIN_NAME}" + +# --- Build --------------------------------------------------------------- + +print_info "Building ${BIN_NAME} from local source (release)..." +if ! cargo build --release; then + die "Build failed. Ensure build dependencies are installed (see README)." +fi + +[[ -x "$BUILT_BIN" ]] || die "Build finished but ${BUILT_BIN} was not produced." + +print_success "Build complete." + +# --- Install binary ------------------------------------------------------ + +prefix_bin="$INSTALL_PREFIX/bin" +SUDO="$(sudo_for "$INSTALL_PREFIX")" + +print_info "Installing binary to $prefix_bin/${BIN_NAME}" +$SUDO mkdir -p "$prefix_bin" +$SUDO cp -f "$BUILT_BIN" "$prefix_bin/${BIN_NAME}" +$SUDO chmod +x "$prefix_bin/${BIN_NAME}" +print_success "Binary installed: $prefix_bin/${BIN_NAME}" + +# --- Done ---------------------------------------------------------------- + +print_success "${BIN_NAME} installed to $prefix_bin" +if [[ ":${PATH}:" != *":$prefix_bin:"* ]]; then + print_warning "$prefix_bin is not in your PATH. Add it: export PATH=\"$prefix_bin:\$PATH\"" +fi + +cat >&2 <&2; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" -BIN_NAME="sovereign-browser" +BIN_NAME="sovereign_browser" BIN_PATH="target/release/${BIN_NAME}" # --- Args --------------------------------------------------------------- diff --git a/src/key_handler.rs b/src/key_handler.rs new file mode 100644 index 0000000..548756b --- /dev/null +++ b/src/key_handler.rs @@ -0,0 +1,245 @@ +//! Browser-level keyboard shortcut dispatch. +//! +//! Port of the `on_key_press` handler from the C project (`main.c:501`). +//! +//! In the C version this handler is connected to **two** places: +//! 1. Each webview's `key-press-event` (in `tab_manager.c` `tab_create()`) +//! 2. The main window's `key-press-event` (in `main.c` setup) +//! +//! The webview connection is required because WebKit consumes key events +//! when the webview has focus, so a window-only handler never sees them. +//! The window connection is a fallback that fires after the webview handler +//! (event propagation: child → parent → window). +//! +//! The `from_webview` flag distinguishes the two connections so that +//! `sovereign://` internal pages (e.g. the settings page's shortcut +//! capture) can receive raw key events on the webview connection while the +//! window connection still processes browser shortcuts. + +use webkit2gtk::*; + +use crate::tab_manager; + +/// Build a WebContext configured for the currently signed-in user. +/// Port of `build_context_for_current_user` (main.c). +pub fn build_context_for_current_user() -> WebContext { + let ctx = crate::web_context::web_context_create(); + let pubkey_hex = crate::menu::app_get_pubkey_hex(); + let signer = crate::menu::app_get_signer(); + let readonly = crate::menu::app_get_readonly(); + if !pubkey_hex.is_empty() { + crate::webkit_data::webkit_data_configure(&ctx, &pubkey_hex); + } + crate::nostr_bridge::nostr_bridge_register(&ctx, signer, &pubkey_hex, readonly); + ctx +} + +/// Handle a key-press event, dispatching browser-level shortcuts. +/// +/// `from_webview` must be `true` when connected to a WebView's +/// `key-press-event` and `false` when connected to the window's. +/// Mirrors the C version (`main.c:501`). +pub fn handle_key_press(event: &gdk::EventKey, from_webview: bool) -> glib::Propagation { + let ctrl = event.state().contains(gdk::ModifierType::CONTROL_MASK); + let shift = event.state().contains(gdk::ModifierType::SHIFT_MASK); + let alt = event.state().contains(gdk::ModifierType::MOD1_MASK); + // Normalize the keyval to lowercase. When Shift is held, GDK delivers + // the uppercase keyval (e.g. `M` for Ctrl+Shift+M), which would never + // match the lowercase `constants::m`. Mirrors the C version's + // `gdk_keyval_to_lower(event->keyval)` (shortcuts.c:199). + let key = event.keyval().to_lower(); + + // When the active tab is showing a sovereign:// internal page (e.g. + // the settings page's keyboard-shortcut capture), bypass shortcut + // interception on the webview connection only — let the event pass + // through to the web page's JS so it can capture arbitrary key combos. + // The window-level connection still processes shortcuts so Ctrl+T, + // Ctrl+N, etc. work on internal pages. Mirrors main.c:516. + if from_webview { + let tabs = tab_manager::tab_manager_get_tabs(); + let active_id = tab_manager::tab_manager_get_active_tab(); + if let Some(tab) = tabs.iter().find(|t| t.id == active_id) { + if tab.url.starts_with("sovereign://") || tab.url.starts_with("about:settings") { + return glib::Propagation::Proceed; + } + } + } + + // Alt+Left/Right (back/forward) — no modifier mask check needed. + if alt && !ctrl && !shift { + match key { + gdk::keys::constants::Left => { + tab_manager::tab_manager_go_back(); + return glib::Propagation::Stop; + } + gdk::keys::constants::Right => { + tab_manager::tab_manager_go_forward(); + return glib::Propagation::Stop; + } + _ => {} + } + } + + // F-key shortcuts (no modifiers). + if !ctrl && !shift && !alt { + match key { + gdk::keys::constants::F5 => { + tab_manager::tab_manager_reload_active(); + return glib::Propagation::Stop; + } + gdk::keys::constants::F11 => { + tab_manager::tab_manager_toggle_fullscreen(); + return glib::Propagation::Stop; + } + _ => {} + } + } + + // Shift+F5 (force reload). + if shift && !ctrl && !alt && key == gdk::keys::constants::F5 { + if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { + if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { + wv.reload_bypass_cache(); + } + } + return glib::Propagation::Stop; + } + + // Ctrl+ key combinations. + if ctrl && !shift && !alt { + match key { + gdk::keys::constants::t => { + if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { + let ctx = build_context_for_current_user(); + tab_manager::tab_manager_new_tab(&nb, &ctx, None); + } + return glib::Propagation::Stop; + } + gdk::keys::constants::n => { + tab_manager::tab_manager_new_window_blank(); + return glib::Propagation::Stop; + } + gdk::keys::constants::o => { + tab_manager::tab_manager_open_file(); + return glib::Propagation::Stop; + } + gdk::keys::constants::w => { + tab_manager::tab_manager_close_active(); + return glib::Propagation::Stop; + } + gdk::keys::constants::l => { + tab_manager::tab_manager_focus_url(); + return glib::Propagation::Stop; + } + gdk::keys::constants::r => { + tab_manager::tab_manager_reload_active(); + return glib::Propagation::Stop; + } + gdk::keys::constants::f => { + if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { + if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { + // WebKit find controller. + // run_javascript is deprecated in webkit2gtk 2.0.2 but + // there is no replacement in the Rust bindings. Suppress. + #[allow(deprecated)] + wv.run_javascript("document.body.innerHTML += '
'; document.getElementById('sb-find-input').focus();", None::<&gio::Cancellable>, |_| {}); + } + } + return glib::Propagation::Stop; + } + gdk::keys::constants::d => { + // Bookmark current page. + if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { + if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { + let url = wv.uri().unwrap_or_default().to_string(); + let title = wv.title().unwrap_or_default().to_string(); + let _ = crate::db::db_add_bookmark(&title, &url, ""); + } + } + return glib::Propagation::Stop; + } + gdk::keys::constants::h => { + tab_manager::tab_manager_open_internal("sovereign://profile"); + return glib::Propagation::Stop; + } + gdk::keys::constants::b => { + tab_manager::tab_manager_open_internal("sovereign://bookmarks"); + return glib::Propagation::Stop; + } + gdk::keys::constants::j => { + tab_manager::tab_manager_open_internal("sovereign://processes"); + return glib::Propagation::Stop; + } + gdk::keys::constants::q => { + crate::session::session_save(); + gtk::main_quit(); + return glib::Propagation::Stop; + } + gdk::keys::constants::comma => { + tab_manager::tab_manager_open_internal("sovereign://settings"); + return glib::Propagation::Stop; + } + gdk::keys::constants::Tab => { + tab_manager::tab_manager_next(); + return glib::Propagation::Stop; + } + gdk::keys::constants::Page_Down => { + tab_manager::tab_manager_next(); + return glib::Propagation::Stop; + } + gdk::keys::constants::Page_Up => { + tab_manager::tab_manager_prev(); + return glib::Propagation::Stop; + } + gdk::keys::constants::plus | gdk::keys::constants::equal => { + tab_manager::tab_manager_zoom_in(); + return glib::Propagation::Stop; + } + gdk::keys::constants::minus => { + tab_manager::tab_manager_zoom_out(); + return glib::Propagation::Stop; + } + gdk::keys::constants::_0 => { + tab_manager::tab_manager_zoom_reset(); + return glib::Propagation::Stop; + } + gdk::keys::constants::Shift_L | gdk::keys::constants::Shift_R => { + return glib::Propagation::Proceed; + } + _ => {} + } + } + + // Ctrl+Shift+ key combinations. + if ctrl && shift && !alt { + match key { + gdk::keys::constants::Tab => { + tab_manager::tab_manager_prev(); + return glib::Propagation::Stop; + } + gdk::keys::constants::i => { + tab_manager::tab_manager_toggle_inspector(); + return glib::Propagation::Stop; + } + gdk::keys::constants::a => { + tab_manager::tab_manager_toggle_sidebar(); + return glib::Propagation::Stop; + } + gdk::keys::constants::m => { + tab_manager::tab_manager_toggle_toolbars(); + return glib::Propagation::Stop; + } + gdk::keys::constants::u => { + crate::menu::menu_switch_identity(); + return glib::Propagation::Stop; + } + gdk::keys::constants::Escape => { + tab_manager::tab_manager_open_internal("sovereign://processes"); + return glib::Propagation::Stop; + } + _ => {} + } + } + + glib::Propagation::Proceed +} diff --git a/src/lib.rs b/src/lib.rs index aa79537..9dc26ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub mod agent_llm; pub mod db; pub mod settings; pub mod tab_manager; +pub mod key_handler; pub mod menu; pub mod session; pub mod bookmarks; diff --git a/src/main.rs b/src/main.rs index 828d0c9..1d93fe4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ mod agent_llm; mod db; mod settings; mod tab_manager; +mod key_handler; mod session; mod bookmarks; mod history; @@ -290,211 +291,10 @@ fn do_cli_login(args: &cli::CliArgs) -> bool { } } +/// Build a WebContext configured for the currently signed-in user. +/// Delegates to the shared implementation in `key_handler`. fn build_context_for_current_user() -> WebContext { - let ctx = web_context::web_context_create(); - let pubkey_hex = menu::app_get_pubkey_hex(); - let signer = menu::app_get_signer(); - let readonly = menu::app_get_readonly(); - if !pubkey_hex.is_empty() { - webkit_data::webkit_data_configure(&ctx, &pubkey_hex); - } - nostr_bridge::nostr_bridge_register(&ctx, signer, &pubkey_hex, readonly); - ctx -} - -/// Handle a key-press event, dispatching browser-level shortcuts. -/// Mirrors the C version (main.c) with all shortcuts. -fn handle_key_press(_window: >k::Window, event: &gdk::EventKey) -> glib::Propagation { - let ctrl = event.state().contains(gdk::ModifierType::CONTROL_MASK); - let shift = event.state().contains(gdk::ModifierType::SHIFT_MASK); - let alt = event.state().contains(gdk::ModifierType::MOD1_MASK); - let key = event.keyval(); - - // When the active tab is showing a sovereign:// internal page (e.g. - // the settings page's keyboard-shortcut capture), bypass shortcut - // interception so the page's JS can capture arbitrary key combos. - let tabs = tab_manager::tab_manager_get_tabs(); - let active_id = tab_manager::tab_manager_get_active_tab(); - if let Some(tab) = tabs.iter().find(|t| t.id == active_id) { - if tab.url.starts_with("sovereign://") || tab.url.starts_with("about:settings") { - return glib::Propagation::Proceed; - } - } - - // Alt+Left/Right (back/forward) — no modifier mask check needed. - if alt && !ctrl && !shift { - match key { - gdk::keys::constants::Left => { - tab_manager::tab_manager_go_back(); - return glib::Propagation::Stop; - } - gdk::keys::constants::Right => { - tab_manager::tab_manager_go_forward(); - return glib::Propagation::Stop; - } - _ => {} - } - } - - // F-key shortcuts (no modifiers). - if !ctrl && !shift && !alt { - match key { - gdk::keys::constants::F5 => { - tab_manager::tab_manager_reload_active(); - return glib::Propagation::Stop; - } - gdk::keys::constants::F11 => { - tab_manager::tab_manager_toggle_fullscreen(); - return glib::Propagation::Stop; - } - _ => {} - } - } - - // Shift+F5 (force reload). - if shift && !ctrl && !alt && key == gdk::keys::constants::F5 { - if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { - if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { - wv.reload_bypass_cache(); - } - } - return glib::Propagation::Stop; - } - - // Ctrl+ key combinations. - if ctrl && !shift && !alt { - match key { - gdk::keys::constants::t => { - if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { - let ctx = build_context_for_current_user(); - tab_manager::tab_manager_new_tab(&nb, &ctx, None); - } - return glib::Propagation::Stop; - } - gdk::keys::constants::n => { - tab_manager::tab_manager_new_window_blank(); - return glib::Propagation::Stop; - } - gdk::keys::constants::o => { - tab_manager::tab_manager_open_file(); - return glib::Propagation::Stop; - } - gdk::keys::constants::w => { - tab_manager::tab_manager_close_active(); - return glib::Propagation::Stop; - } - gdk::keys::constants::l => { - tab_manager::tab_manager_focus_url(); - return glib::Propagation::Stop; - } - gdk::keys::constants::r => { - tab_manager::tab_manager_reload_active(); - return glib::Propagation::Stop; - } - gdk::keys::constants::f => { - if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { - if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { - // WebKit find controller. - wv.run_javascript("document.body.innerHTML += '
'; document.getElementById('sb-find-input').focus();", None::<&gio::Cancellable>, |_| {}); - } - } - return glib::Propagation::Stop; - } - gdk::keys::constants::d => { - // Bookmark current page. - if let Some(nb) = tab_manager::tab_manager_get_active_notebook() { - if let Some(wv) = tab_manager::tab_manager_get_active_webview(&nb) { - let url = wv.uri().unwrap_or_default().to_string(); - let title = wv.title().unwrap_or_default().to_string(); - let _ = db::db_add_bookmark(&title, &url, ""); - } - } - return glib::Propagation::Stop; - } - gdk::keys::constants::h => { - tab_manager::tab_manager_open_internal("sovereign://profile"); - return glib::Propagation::Stop; - } - gdk::keys::constants::b => { - tab_manager::tab_manager_open_internal("sovereign://bookmarks"); - return glib::Propagation::Stop; - } - gdk::keys::constants::j => { - tab_manager::tab_manager_open_internal("sovereign://processes"); - return glib::Propagation::Stop; - } - gdk::keys::constants::q => { - session::session_save(); - gtk::main_quit(); - return glib::Propagation::Stop; - } - gdk::keys::constants::comma => { - tab_manager::tab_manager_open_internal("sovereign://settings"); - return glib::Propagation::Stop; - } - gdk::keys::constants::Tab => { - tab_manager::tab_manager_next(); - return glib::Propagation::Stop; - } - gdk::keys::constants::Page_Down => { - tab_manager::tab_manager_next(); - return glib::Propagation::Stop; - } - gdk::keys::constants::Page_Up => { - tab_manager::tab_manager_prev(); - return glib::Propagation::Stop; - } - gdk::keys::constants::plus | gdk::keys::constants::equal => { - tab_manager::tab_manager_zoom_in(); - return glib::Propagation::Stop; - } - gdk::keys::constants::minus => { - tab_manager::tab_manager_zoom_out(); - return glib::Propagation::Stop; - } - gdk::keys::constants::_0 => { - tab_manager::tab_manager_zoom_reset(); - return glib::Propagation::Stop; - } - gdk::keys::constants::Shift_L | gdk::keys::constants::Shift_R => { - return glib::Propagation::Proceed; - } - _ => {} - } - } - - // Ctrl+Shift+ key combinations. - if ctrl && shift && !alt { - match key { - gdk::keys::constants::Tab => { - tab_manager::tab_manager_prev(); - return glib::Propagation::Stop; - } - gdk::keys::constants::i => { - tab_manager::tab_manager_toggle_inspector(); - return glib::Propagation::Stop; - } - gdk::keys::constants::a => { - tab_manager::tab_manager_toggle_sidebar(); - return glib::Propagation::Stop; - } - gdk::keys::constants::m => { - tab_manager::tab_manager_toggle_toolbars(); - return glib::Propagation::Stop; - } - gdk::keys::constants::u => { - crate::menu::menu_switch_identity(); - return glib::Propagation::Stop; - } - gdk::keys::constants::Escape => { - tab_manager::tab_manager_open_internal("sovereign://processes"); - return glib::Propagation::Stop; - } - _ => {} - } - } - - glib::Propagation::Proceed + key_handler::build_context_for_current_user() } fn main() { @@ -597,10 +397,12 @@ fn main() { } } - // Wire keyboard shortcuts. - let window_shortcuts = window.clone(); + // Wire keyboard shortcuts on the window (fallback connection). The + // primary connection is on each webview (see tab_manager_new_tab), + // because WebKit consumes key events when the webview has focus. + // Mirrors the C version (main.c:1054 + tab_manager.c:3076). window.connect_key_press_event(move |_w, event| { - handle_key_press(&window_shortcuts, event) + key_handler::handle_key_press(event, false) }); window.connect_delete_event(|_, _| { diff --git a/src/nostr_scheme.rs b/src/nostr_scheme.rs index b10ae72..273e736 100644 --- a/src/nostr_scheme.rs +++ b/src/nostr_scheme.rs @@ -6,6 +6,7 @@ use webkit2gtk::*; use gio::MemoryInputStream; use glib::Bytes; +use futures_util::StreamExt; use std::thread; use crate::nostr_url; @@ -516,25 +517,28 @@ fn handle_nostr_scheme(request: &URISchemeRequest) { // Create job and run in thread let job = SchemeJob::new(entity_str.clone(), decoded); - // Use glib's MainContext channel to send the result back to the main thread - let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); + // Send the result back to the main thread via an async channel, then + // handle it on the main context. (glib::MainContext::channel is + // deprecated in favour of async channels + spawn_local.) + let (tx, mut rx) = futures_channel::mpsc::unbounded(); thread::spawn(move || { let completed_job = scheme_worker(job); - let _ = tx.send(completed_job); + let _ = tx.unbounded_send(completed_job); }); - // Attach the receiver to the main context to handle the response + // Handle the response on the main context. let request_clone = request.clone(); - rx.attach(None, move |completed_job| { - let json = completed_job.response_json.unwrap_or_else(|| { - serde_json::json!({ - "error": "Internal error", - "entity": completed_job.entity - }).to_string() - }); - respond_json(&request_clone, &json); - glib::ControlFlow::Break + glib::MainContext::default().spawn_local(async move { + if let Some(completed_job) = rx.next().await { + let json = completed_job.response_json.unwrap_or_else(|| { + serde_json::json!({ + "error": "Internal error", + "entity": completed_job.entity + }).to_string() + }); + respond_json(&request_clone, &json); + } }); } diff --git a/src/tab_manager.rs b/src/tab_manager.rs index 0df0618..b0a4151 100644 --- a/src/tab_manager.rs +++ b/src/tab_manager.rs @@ -238,6 +238,15 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti ws.set_hardware_acceleration_policy(webkit2gtk::HardwareAccelerationPolicy::Always); } + // Connect the browser shortcut handler to the webview. This is the + // primary connection: WebKit consumes key events when the webview has + // focus, so a window-only handler never sees them. Mirrors the C + // version (tab_manager.c:3076). The window connection in main.rs is + // the fallback. + webview.connect_key_press_event(|_wv, event| { + crate::key_handler::handle_key_press(event, true) + }); + // Build the per-tab page: toolbar on top, webview below. let page = gtk::Box::new(gtk::Orientation::Vertical, 0); @@ -389,6 +398,17 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti // are not automatically visible. page.show_all(); tab_label.show_all(); + + // Apply the current chrome visibility so a tab opened while the + // toolbars are hidden stays hidden. Mirrors the C version + // (tab_manager.c:3235). The tab strip itself is per-notebook + // (set_show_tabs), already handled by the toggle; here we sync the + // per-tab toolbar + bookmark bar. + if !*G_TOOLBARS_VISIBLE.lock().unwrap() { + toolbar.set_visible(false); + bookmark_bar.set_visible(false); + } + let page_num = notebook.n_pages(); notebook.set_current_page(Some(if page_num > 0 { page_num - 1 } else { 0 })); notebook.set_tab_reorderable(&page, s.tab_drag_reorder); @@ -659,7 +679,21 @@ pub fn tab_manager_new_tab(notebook: >k::Notebook, ctx: &WebContext, url: Opti context_menu.insert(&item, 0); } - // Return false to let WebKit show the default menu (with our item added). + // "Toggle Toolbars" — lets the user restore the hidden chrome + // (tab strip, toolbar, bookmark bar) from the page context menu, + // which is reachable even when the toolbars are hidden. + let toggle_action = gio::SimpleAction::new("toggle-toolbars", None); + toggle_action.connect_activate(|_, _| { + tab_manager_toggle_toolbars(); + }); + let toggle_item = webkit2gtk::ContextMenuItem::from_gaction( + &toggle_action, + "Toggle Toolbars", + None, + ); + context_menu.append(&toggle_item); + + // Return false to let WebKit show the default menu (with our items added). false }); @@ -1307,6 +1341,11 @@ fn build_hamburger_menu() -> gtk::MenuButton { item_sidebar.connect_activate(|_| tab_manager_toggle_sidebar()); menu.append(&item_sidebar); + let item_toolbars = gtk::MenuItem::with_label("Toggle Toolbars"); + item_toolbars.set_tooltip_text(Some("Show or hide the tab strip, toolbars, and bookmark bars (Ctrl+Shift+M)")); + item_toolbars.connect_activate(|_| tab_manager_toggle_toolbars()); + menu.append(&item_toolbars); + menu.append(>k::SeparatorMenuItem::new()); // ── Internal pages ────────────────────────────────────────────── @@ -1493,17 +1532,42 @@ fn create_sidebar_webview(container: >k::Box) { webview.load_uri(AGENT_CHAT_URL); } -/// Toggle toolbar visibility on the active window. +/// Whether the browser chrome (tab strip, toolbars, bookmark bars) is +/// currently visible. Mirrors the C `window_state_t.toolbars_visible` +/// (tab_manager.c:4322). Starts visible. +static G_TOOLBARS_VISIBLE: Lazy> = Lazy::new(|| Mutex::new(true)); + +/// Toggle browser chrome visibility on the active window. +/// +/// Mirrors the C `tab_manager_toggle_toolbars()` (tab_manager.c:4318): +/// hides/shows the tab strip (GtkNotebook header), the per-tab toolbar, +/// and the per-tab bookmark bar, leaving only the page content. Pressing +/// the shortcut again restores them. pub fn tab_manager_toggle_toolbars() { - if let Some(nb) = tab_manager_get_active_notebook() { - let n = nb.n_pages(); - for i in 0..n { - if let Some(page) = nb.nth_page(Some(i)) { - if let Ok(box_) = page.downcast::() { - for child in box_.children() { - if child.widget_name() == "main-toolbar" { - child.set_visible(!child.is_visible()); - } + let nb = match tab_manager_get_active_notebook() { + Some(n) => n, + None => return, + }; + + // Flip the shared visibility flag. + let vis = { + let mut visible = G_TOOLBARS_VISIBLE.lock().unwrap(); + *visible = !*visible; + *visible + }; + + // Tab strip (GtkNotebook header) — per-notebook. + nb.set_show_tabs(vis); + + // Per-tab toolbar + bookmark bar. + let n = nb.n_pages(); + for i in 0..n { + if let Some(page) = nb.nth_page(Some(i)) { + if let Ok(box_) = page.downcast::() { + for child in box_.children() { + let name = child.widget_name(); + if name == "main-toolbar" || name == "bookmark-bar" { + child.set_visible(vis); } } } diff --git a/src/version.rs b/src/version.rs index 5480393..17b2c2a 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,7 +1,7 @@ //! Version information for sovereign_browser /// The current version of sovereign_browser (with leading 'v'). -pub const VERSION: &str = "v0.0.5"; +pub const VERSION: &str = "v0.0.6"; /// Major version number. pub const VERSION_MAJOR: u32 = 0; @@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0; pub const VERSION_MINOR: u32 = 0; /// Patch version number. -pub const VERSION_PATCH: u32 = 5; +pub const VERSION_PATCH: u32 = 6;