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:
Generated
+3
-2
@@ -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
@@ -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"] }
|
||||||
|
|||||||
@@ -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 ---------------------------------------------------------------
|
||||||
|
|||||||
Executable
+118
@@ -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
|
||||||
@@ -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 ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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: >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 += '<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
@@ -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
@@ -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);
|
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: >k::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: >k::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(>k::SeparatorMenuItem::new());
|
menu.append(>k::SeparatorMenuItem::new());
|
||||||
|
|
||||||
// ── Internal pages ──────────────────────────────────────────────
|
// ── Internal pages ──────────────────────────────────────────────
|
||||||
@@ -1493,17 +1532,42 @@ fn create_sidebar_webview(container: >k::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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user