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;