v0.0.6 - Fix Ctrl+Shift+M toolbar toggle (uppercase keyval) and add Toggle Toolbars to hamburger and page context menus

This commit is contained in:
Laan Tungir
2026-09-17 14:48:04 -04:00
parent d3ece56cba
commit d131762ce8
12 changed files with 477 additions and 241 deletions
Generated
+3 -2
View File
@@ -3055,8 +3055,8 @@ dependencies = [
] ]
[[package]] [[package]]
name = "sovereign-browser" name = "sovereign_browser"
version = "0.0.4" version = "0.0.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@@ -3064,6 +3064,7 @@ dependencies = [
"cairo-rs", "cairo-rs",
"chrono", "chrono",
"env_logger", "env_logger",
"futures-channel",
"futures-util", "futures-util",
"gdk", "gdk",
"gdk-pixbuf", "gdk-pixbuf",
+3 -2
View File
@@ -5,8 +5,8 @@ members = [
] ]
[package] [package]
name = "sovereign-browser" name = "sovereign_browser"
version = "0.0.5" version = "0.0.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "A Linux x86 web browser built on WebKitGTK with Nostr identity" description = "A Linux x86 web browser built on WebKitGTK with Nostr identity"
@@ -32,6 +32,7 @@ pango = "0.18"
# ── Async Runtime ─────────────────────────────────────────────────── # ── Async Runtime ───────────────────────────────────────────────────
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
futures-util = "0.3" futures-util = "0.3"
futures-channel = "0.3"
# ── Serialization ─────────────────────────────────────────────────── # ── Serialization ───────────────────────────────────────────────────
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
+1 -1
View File
@@ -1 +1 @@
0.0.5 0.0.6
+1 -1
View File
@@ -45,7 +45,7 @@ register_cleanup() {
# Gitea repo for release uploads (derived from the origin remote). # Gitea repo for release uploads (derived from the origin remote).
GITEA_API_URL="https://git.laantungir.net/api/v1/repos/laantungir/sovereign_browser_rust" 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}" RELEASE_BIN_PATH="target/release/${BIN_NAME}"
# --- Args --------------------------------------------------------------- # --- Args ---------------------------------------------------------------
+118
View File
@@ -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 <dir> 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 <<EOF
${GREEN}[SUCCESS]${NC} Next steps:
Run: ${BIN_NAME}
Or: ${BIN_NAME} --login-method generate
Or: ${BIN_NAME} --login-method generate --url https://example.com
EOF
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
set -e set -e
# run.sh - Build (only if needed) and run the sovereign-browser binary. # run.sh - Build (only if needed) and run the sovereign_browser binary.
# #
# Usage: # Usage:
# ./run.sh [args...] # build if stale, then run with args # ./run.sh [args...] # build if stale, then run with args
@@ -28,7 +28,7 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
BIN_NAME="sovereign-browser" BIN_NAME="sovereign_browser"
BIN_PATH="target/release/${BIN_NAME}" BIN_PATH="target/release/${BIN_NAME}"
# --- Args --------------------------------------------------------------- # --- Args ---------------------------------------------------------------
+245
View File
@@ -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 += '<div id=\"sb-find-bar\" style=\"position:fixed;top:0;left:0;right:0;z-index:9999;background:#333;color:#fff;padding:8px;\"><input id=\"sb-find-input\" type=\"text\" placeholder=\"Find in page…\" style=\"width:80%;padding:4px;\"></div>'; 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
}
+1
View File
@@ -43,6 +43,7 @@ pub mod agent_llm;
pub mod db; pub mod db;
pub mod settings; pub mod settings;
pub mod tab_manager; pub mod tab_manager;
pub mod key_handler;
pub mod menu; pub mod menu;
pub mod session; pub mod session;
pub mod bookmarks; pub mod bookmarks;
+9 -207
View File
@@ -34,6 +34,7 @@ mod agent_llm;
mod db; mod db;
mod settings; mod settings;
mod tab_manager; mod tab_manager;
mod key_handler;
mod session; mod session;
mod bookmarks; mod bookmarks;
mod history; 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 { fn build_context_for_current_user() -> WebContext {
let ctx = web_context::web_context_create(); key_handler::build_context_for_current_user()
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: &gtk::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 += '<div id=\"sb-find-bar\" style=\"position:fixed;top:0;left:0;right:0;z-index:9999;background:#333;color:#fff;padding:8px;\"><input id=\"sb-find-input\" type=\"text\" placeholder=\"Find in page…\" style=\"width:80%;padding:4px;\"></div>'; 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
} }
fn main() { fn main() {
@@ -597,10 +397,12 @@ fn main() {
} }
} }
// Wire keyboard shortcuts. // Wire keyboard shortcuts on the window (fallback connection). The
let window_shortcuts = window.clone(); // 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| { 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(|_, _| { window.connect_delete_event(|_, _| {
+17 -13
View File
@@ -6,6 +6,7 @@
use webkit2gtk::*; use webkit2gtk::*;
use gio::MemoryInputStream; use gio::MemoryInputStream;
use glib::Bytes; use glib::Bytes;
use futures_util::StreamExt;
use std::thread; use std::thread;
use crate::nostr_url; use crate::nostr_url;
@@ -516,25 +517,28 @@ fn handle_nostr_scheme(request: &URISchemeRequest) {
// Create job and run in thread // Create job and run in thread
let job = SchemeJob::new(entity_str.clone(), decoded); let job = SchemeJob::new(entity_str.clone(), decoded);
// Use glib's MainContext channel to send the result back to the main thread // Send the result back to the main thread via an async channel, then
let (tx, rx) = glib::MainContext::channel(glib::Priority::default()); // 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 || { thread::spawn(move || {
let completed_job = scheme_worker(job); 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(); let request_clone = request.clone();
rx.attach(None, move |completed_job| { glib::MainContext::default().spawn_local(async move {
let json = completed_job.response_json.unwrap_or_else(|| { if let Some(completed_job) = rx.next().await {
serde_json::json!({ let json = completed_job.response_json.unwrap_or_else(|| {
"error": "Internal error", serde_json::json!({
"entity": completed_job.entity "error": "Internal error",
}).to_string() "entity": completed_job.entity
}); }).to_string()
respond_json(&request_clone, &json); });
glib::ControlFlow::Break respond_json(&request_clone, &json);
}
}); });
} }
+75 -11
View File
@@ -238,6 +238,15 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
ws.set_hardware_acceleration_policy(webkit2gtk::HardwareAccelerationPolicy::Always); 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. // Build the per-tab page: toolbar on top, webview below.
let page = gtk::Box::new(gtk::Orientation::Vertical, 0); let page = gtk::Box::new(gtk::Orientation::Vertical, 0);
@@ -389,6 +398,17 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
// are not automatically visible. // are not automatically visible.
page.show_all(); page.show_all();
tab_label.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(); let page_num = notebook.n_pages();
notebook.set_current_page(Some(if page_num > 0 { page_num - 1 } else { 0 })); notebook.set_current_page(Some(if page_num > 0 { page_num - 1 } else { 0 }));
notebook.set_tab_reorderable(&page, s.tab_drag_reorder); notebook.set_tab_reorderable(&page, s.tab_drag_reorder);
@@ -659,7 +679,21 @@ pub fn tab_manager_new_tab(notebook: &gtk::Notebook, ctx: &WebContext, url: Opti
context_menu.insert(&item, 0); 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 false
}); });
@@ -1307,6 +1341,11 @@ fn build_hamburger_menu() -> gtk::MenuButton {
item_sidebar.connect_activate(|_| tab_manager_toggle_sidebar()); item_sidebar.connect_activate(|_| tab_manager_toggle_sidebar());
menu.append(&item_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(&gtk::SeparatorMenuItem::new()); menu.append(&gtk::SeparatorMenuItem::new());
// ── Internal pages ────────────────────────────────────────────── // ── Internal pages ──────────────────────────────────────────────
@@ -1493,17 +1532,42 @@ fn create_sidebar_webview(container: &gtk::Box) {
webview.load_uri(AGENT_CHAT_URL); 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<Mutex<bool>> = 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() { pub fn tab_manager_toggle_toolbars() {
if let Some(nb) = tab_manager_get_active_notebook() { let nb = match tab_manager_get_active_notebook() {
let n = nb.n_pages(); Some(n) => n,
for i in 0..n { None => return,
if let Some(page) = nb.nth_page(Some(i)) { };
if let Ok(box_) = page.downcast::<gtk::Box>() {
for child in box_.children() { // Flip the shared visibility flag.
if child.widget_name() == "main-toolbar" { let vis = {
child.set_visible(!child.is_visible()); 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::<gtk::Box>() {
for child in box_.children() {
let name = child.widget_name();
if name == "main-toolbar" || name == "bookmark-bar" {
child.set_visible(vis);
} }
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
//! Version information for sovereign_browser //! Version information for sovereign_browser
/// The current version of sovereign_browser (with leading 'v'). /// 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. /// Major version number.
pub const VERSION_MAJOR: u32 = 0; pub const VERSION_MAJOR: u32 = 0;
@@ -10,4 +10,4 @@ pub const VERSION_MAJOR: u32 = 0;
pub const VERSION_MINOR: u32 = 0; pub const VERSION_MINOR: u32 = 0;
/// Patch version number. /// Patch version number.
pub const VERSION_PATCH: u32 = 5; pub const VERSION_PATCH: u32 = 6;