From aaa8096f4a89d9ce94555441f14643dc0b4fe46e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 18 Jul 2026 17:33:02 +0200 Subject: [PATCH] Added documentation --- runtime/src/global_shortcuts.rs | 125 +++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/runtime/src/global_shortcuts.rs b/runtime/src/global_shortcuts.rs index a7df7597..a2e97c58 100644 --- a/runtime/src/global_shortcuts.rs +++ b/runtime/src/global_shortcuts.rs @@ -17,33 +17,42 @@ use crate::app_window::{Event, TauriEventType}; #[cfg(target_os = "linux")] use ashpd::desktop::{CreateSessionOptions, ResponseError}; + #[cfg(target_os = "linux")] use ashpd::desktop::global_shortcuts::{ BindShortcutsOptions, GlobalShortcuts, ListShortcutsOptions, NewShortcut, }; -#[cfg(target_os = "linux")] + #[cfg(target_os = "linux")] use futures::StreamExt; +/// Serializes access to the active shortcut bindings across API requests. static SHORTCUT_MANAGER: Lazy> = Lazy::new(|| Mutex::new(ShortcutManager::default())); +/// Indicates whether shortcut activations must currently be ignored. static PROCESSING_SUSPENDED: AtomicBool = AtomicBool::new(false); #[cfg(target_os = "linux")] +/// Supplies unique generations for portal sessions so stale signal tasks can be ignored. static NEXT_PORTAL_GENERATION: AtomicU64 = AtomicU64::new(1); #[cfg(target_os = "linux")] +/// Maps each shortcut to the generation of its currently active portal session. static ACTIVE_PORTAL_GENERATIONS: Lazy>> = Lazy::new(|| std::sync::Mutex::new(HashMap::new())); /// Enum identifying global keyboard shortcuts. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] pub enum Shortcut { + /// Null value used when no supported shortcut was specified. None = 0, + + /// Toggles voice recording and transcription. VoiceRecordingToggle, } impl Shortcut { + /// Resolves an application-provided portal shortcut ID to its internal identifier. #[cfg(target_os = "linux")] fn from_portal_id(id: &str) -> Option { match id { @@ -56,9 +65,16 @@ impl Shortcut { /// Request payload for registering or disabling a global shortcut. #[derive(Clone, Deserialize)] pub struct RegisterShortcutRequest { + /// Identifies the action controlled by the shortcut. pub id: Shortcut, + + /// Contains the preferred key combination in Tauri shortcut syntax. pub shortcut: String, + + /// Contains the localized action description shown by the desktop portal. pub description: String, + + /// Indicates that the user deliberately requested a different key combination. pub reconfigure: bool, } @@ -66,22 +82,37 @@ pub struct RegisterShortcutRequest { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] #[serde(rename_all = "snake_case")] pub enum ShortcutBackend { + /// No native shortcut backend is active. None, + + /// The XDG Desktop Portal manages the shortcut. Portal, + + /// The Tauri global-shortcut plugin manages the shortcut. Tauri, } /// Response for shortcut registration and processing state changes. #[derive(Serialize)] pub struct ShortcutResponse { + /// Indicates whether the requested operation completed successfully. pub success: bool, + + /// Contains a technical error description when the operation failed. pub error_message: String, + + /// Identifies the backend involved in the operation. pub backend: ShortcutBackend, + + /// Indicates whether the user cancelled the portal request. pub cancelled: bool, + + /// Contains the effective, user-facing shortcut label selected by the backend. pub effective_display_name: String, } impl ShortcutResponse { + /// Creates a successful shortcut response for the selected backend. fn success(backend: ShortcutBackend, effective_display_name: String) -> Self { Self { success: true, @@ -92,6 +123,7 @@ impl ShortcutResponse { } } + /// Creates a failed shortcut response with its backend and cancellation state. fn error(error_message: impl Into, backend: ShortcutBackend, cancelled: bool) -> Self { Self { success: false, @@ -104,22 +136,36 @@ impl ShortcutResponse { } #[derive(Default)] +/// Owns all currently active shortcut bindings. struct ShortcutManager { + /// Maps each logical shortcut to its active backend binding. bindings: HashMap, } +/// Stores the backend-specific resources required by an active shortcut. enum ActiveBinding { - Tauri { shortcut: String }, - #[cfg(target_os = "linux")] - Portal { + /// Stores a shortcut registered through the Tauri plugin. + Tauri { + /// Contains the registered shortcut in Tauri syntax. shortcut: String, + }, + + #[cfg(target_os = "linux")] + /// Stores a shortcut and its live XDG portal session. + Portal { + /// Contains the preferred shortcut in Tauri syntax. + shortcut: String, + /// Contains the effective human-readable trigger selected by the portal. effective_display_name: String, + /// Distinguishes this session from superseded portal signal tasks. generation: u64, + /// Keeps the portal registration active for the binding's lifetime. session: ashpd::desktop::Session, }, } impl ActiveBinding { + /// Returns the preferred Tauri-format shortcut associated with the binding. fn shortcut(&self) -> &str { match self { Self::Tauri { shortcut } => shortcut, @@ -128,6 +174,7 @@ impl ActiveBinding { } } + /// Returns the native backend used by the binding. fn backend(&self) -> ShortcutBackend { match self { Self::Tauri { .. } => ShortcutBackend::Tauri, @@ -136,6 +183,7 @@ impl ActiveBinding { } } + /// Returns the shortcut label that should be displayed in the UI. fn effective_display_name(&self) -> String { match self { Self::Tauri { shortcut } => shortcut.clone(), @@ -145,6 +193,7 @@ impl ActiveBinding { } } +/// Returns a snapshot of all registered shortcut IDs and their preferred combinations. pub async fn registered_shortcuts() -> Vec<(Shortcut, String)> { SHORTCUT_MANAGER .lock() @@ -155,6 +204,7 @@ pub async fn registered_shortcuts() -> Vec<(Shortcut, String)> { .collect() } +/// Registers, reconfigures, or disables a global shortcut through the appropriate backend. pub async fn register( app_handle: Option, event_sender: Option>, @@ -232,10 +282,12 @@ pub async fn register( } } +/// Determines whether an existing registration already satisfies the request. fn registration_is_unchanged(current: Option<&str>, requested: &str, reconfigure: bool) -> bool { current.is_some_and(|current| current.eq_ignore_ascii_case(requested)) && !reconfigure } +/// Removes an active binding and returns a successful disabled response. async fn disable_binding( app_handle: &tauri::AppHandle, manager: &mut ShortcutManager, @@ -250,6 +302,7 @@ async fn disable_binding( } #[cfg(target_os = "linux")] +/// Activates a prepared portal binding before closing the superseded binding. async fn replace_portal_binding( app_handle: &tauri::AppHandle, manager: &mut ShortcutManager, @@ -267,6 +320,7 @@ async fn replace_portal_binding( } } +/// Releases the native resources owned by an active shortcut binding. async fn close_binding(app_handle: &tauri::AppHandle, id: Shortcut, binding: ActiveBinding) { match binding { ActiveBinding::Tauri { shortcut } => { @@ -288,6 +342,7 @@ async fn close_binding(app_handle: &tauri::AppHandle, id: Shortcut, binding: Act } } +/// Registers a shortcut callback through the Tauri global-shortcut plugin. fn register_tauri_binding( app_handle: &tauri::AppHandle, shortcut: &str, @@ -303,6 +358,7 @@ fn register_tauri_binding( }) } +/// Publishes a shortcut activation using the existing runtime event format. fn send_shortcut_pressed(event_sender: &broadcast::Sender, shortcut_id: Shortcut, source: &str) { info!(Source = "Global shortcuts"; "Global shortcut triggered through {source} for '{}'.", shortcut_id); if let Err(error) = event_sender.send(Event::new( @@ -313,6 +369,7 @@ fn send_shortcut_pressed(event_sender: &broadcast::Sender, shortcut_id: S } } +/// Suspends shortcut processing while preserving portal sessions for later use. pub async fn suspend(app_handle: Option) -> ShortcutResponse { PROCESSING_SUSPENDED.store(true, Ordering::Relaxed); let Some(app_handle) = app_handle else { @@ -333,6 +390,7 @@ pub async fn suspend(app_handle: Option) -> ShortcutResponse { ShortcutResponse::success(ShortcutBackend::None, String::new()) } +/// Resumes shortcut processing and restores shortcuts owned by the Tauri backend. pub async fn resume( app_handle: Option, event_sender: Option>, @@ -365,27 +423,42 @@ pub async fn resume( #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(not(target_os = "linux"), allow(dead_code))] +/// Classifies portal failures so fallback and user feedback remain intentional. enum PortalFailureKind { + /// The portal service or GlobalShortcuts interface is not available. Unavailable, + + /// The user cancelled the portal interaction. Cancelled, + + /// The portal explicitly denied the shortcut request. Denied, + + /// The portal failed for another technical reason. Technical, } +/// Determines whether an unavailable portal may safely fall back to Tauri. fn may_fallback_to_tauri(failure: PortalFailureKind, current_backend: Option) -> bool { failure == PortalFailureKind::Unavailable && current_backend.is_none_or(|backend| backend == ShortcutBackend::Tauri) } +/// Determines whether a backend must unregister its shortcut during suspension. fn unregister_backend_during_suspend(backend: ShortcutBackend) -> bool { backend == ShortcutBackend::Tauri } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Describes how a newly created portal session should obtain its shortcut. enum PortalBindingAction { + /// Reuse a shortcut that the portal restored from an earlier session. Restore, + + /// Ask the portal to bind or deliberately reconfigure the shortcut. Bind, } +/// Selects restore or bind based on portal state and explicit user intent. fn portal_binding_action(was_restored: bool, reconfigure: bool) -> PortalBindingAction { if was_restored && !reconfigure { PortalBindingAction::Restore @@ -396,13 +469,18 @@ fn portal_binding_action(was_restored: bool, reconfigure: bool) -> PortalBinding #[derive(Debug, Clone)] #[cfg_attr(not(target_os = "linux"), allow(dead_code))] +/// Carries a classified portal failure and its technical description. struct PortalFailure { + /// Identifies the semantic failure category. kind: PortalFailureKind, + + /// Contains the technical error text used for logging and API responses. message: String, } impl PortalFailure { #[cfg(target_os = "linux")] + /// Converts an `ashpd` error into the application's portal failure categories. fn from_error(error: ashpd::Error) -> Self { let kind = match &error { ashpd::Error::Response(ResponseError::Cancelled) => PortalFailureKind::Cancelled, @@ -416,20 +494,28 @@ impl PortalFailure { Self { kind, message: error.to_string() } } + /// Creates an explicit portal-permission denial. fn denied(message: impl Into) -> Self { Self { kind: PortalFailureKind::Denied, message: message.into() } } } #[derive(Debug, Clone)] +/// Contains the portal-facing ID and effective label of a registered shortcut. struct PortalShortcutInfo { + /// Contains the stable application-provided shortcut ID. id: String, + + /// Contains the human-readable trigger returned by the portal. effective_display_name: String, } +/// Abstracts portal listing and binding operations for deterministic lifecycle tests. trait PortalAdapter { + /// Lists shortcuts restored into the current portal session. async fn list_shortcuts(&mut self) -> Result, PortalFailure>; + /// Binds a shortcut with a localized description and preferred XDG trigger. async fn bind_shortcut( &mut self, id: &str, @@ -438,6 +524,7 @@ trait PortalAdapter { ) -> Result, PortalFailure>; } +/// Restores an approved portal shortcut or binds it when required. async fn resolve_portal_shortcut( adapter: &mut A, request: &RegisterShortcutRequest, @@ -469,13 +556,18 @@ async fn resolve_portal_shortcut( } #[cfg(target_os = "linux")] +/// Implements portal operations through `ashpd` for one live session. struct AshpdPortalAdapter<'a> { + /// Provides access to the GlobalShortcuts portal interface. portal: &'a GlobalShortcuts, + + /// Identifies the session whose shortcuts are listed or bound. session: &'a ashpd::desktop::Session, } #[cfg(target_os = "linux")] impl PortalAdapter for AshpdPortalAdapter<'_> { + /// Lists and normalizes shortcuts restored by the XDG portal. async fn list_shortcuts(&mut self) -> Result, PortalFailure> { let response = self .portal @@ -494,6 +586,7 @@ impl PortalAdapter for AshpdPortalAdapter<'_> { .collect()) } + /// Binds one shortcut through the XDG portal and normalizes its response. async fn bind_shortcut( &mut self, id: &str, @@ -520,6 +613,7 @@ impl PortalAdapter for AshpdPortalAdapter<'_> { } #[cfg(target_os = "linux")] +/// Detects D-Bus error strings that specifically indicate an unavailable portal. fn portal_error_is_unavailable(message: &str) -> bool { let normalized = message.to_ascii_lowercase(); normalized.contains("unknownmethod") @@ -529,6 +623,7 @@ fn portal_error_is_unavailable(message: &str) -> bool { } #[cfg(target_os = "linux")] +/// Prepares a complete portal session and its signal listeners without replacing the active binding. async fn prepare_portal_binding( request: &RegisterShortcutRequest, event_sender: broadcast::Sender, @@ -600,6 +695,7 @@ async fn prepare_portal_binding( }) } +/// Converts a Tauri-format shortcut into the XDG shortcuts specification format. fn tauri_shortcut_to_xdg(shortcut: &str) -> Result { let mut converted = Vec::new(); let parts: Vec<&str> = shortcut.split('+').collect(); @@ -668,6 +764,7 @@ fn tauri_shortcut_to_xdg(shortcut: &str) -> Result { Ok(converted.join("+")) } +/// Converts Tauri numpad key names into XKB keypad key symbols. fn numpad_key_to_xdg(key: &str) -> Option { let suffix = match key { "num0" => "0", @@ -699,19 +796,27 @@ mod tests { use super::*; #[derive(Default)] + /// Simulates portal listing and binding outcomes for lifecycle tests. struct FakePortalAdapter { + /// Shortcuts returned by the simulated list operation. listed: Vec, + /// Shortcuts returned by the simulated bind operation. bound: Vec, + /// Optional failure returned instead of a successful bind result. bind_failure: Option, + /// Counts bind calls so tests can detect unnecessary portal dialogs. bind_calls: usize, + /// Records the preferred trigger supplied to the simulated portal. last_preferred_trigger: String, } impl PortalAdapter for FakePortalAdapter { + /// Returns the shortcuts configured as restored by the fake portal. async fn list_shortcuts(&mut self) -> Result, PortalFailure> { Ok(self.listed.clone()) } + /// Records the bind request and returns the configured result or failure. async fn bind_shortcut( &mut self, _id: &str, @@ -727,6 +832,7 @@ mod tests { } } + /// Creates a voice-recording shortcut request for portal lifecycle tests. fn portal_request(shortcut: &str, reconfigure: bool) -> RegisterShortcutRequest { RegisterShortcutRequest { id: Shortcut::VoiceRecordingToggle, @@ -736,6 +842,7 @@ mod tests { } } + /// Creates normalized portal shortcut data for test responses. fn portal_shortcut(display_name: &str) -> PortalShortcutInfo { PortalShortcutInfo { id: Shortcut::VoiceRecordingToggle.to_string(), @@ -744,6 +851,7 @@ mod tests { } #[test] + /// Verifies conversion from representative Tauri combinations to XDG triggers. fn converts_tauri_shortcut_to_xdg_trigger() { assert_eq!(tauri_shortcut_to_xdg("CmdOrControl+Shift+1").unwrap(), "CTRL+SHIFT+1"); assert_eq!(tauri_shortcut_to_xdg("Control+Alt+Enter").unwrap(), "CTRL+ALT+Return"); @@ -754,12 +862,14 @@ mod tests { } #[test] + /// Verifies that unsupported modifiers and keys are rejected during conversion. fn rejects_unsupported_xdg_trigger_parts() { assert!(tauri_shortcut_to_xdg("Hyper+1").is_err()); assert!(tauri_shortcut_to_xdg("Ctrl++").is_err()); } #[test] + /// Verifies that unchanged settings do not cause duplicate native registrations. fn identical_configuration_is_not_registered_twice() { assert!(registration_is_unchanged(Some("CmdOrControl+Shift+1"), "cmdorcontrol+shift+1", false)); assert!(!registration_is_unchanged(Some("CmdOrControl+Shift+1"), "CmdOrControl+Shift+2", false)); @@ -767,6 +877,7 @@ mod tests { } #[tokio::test] + /// Verifies that an approved shortcut is restored without another bind request. async fn portal_adapter_restores_without_binding() { let mut adapter = FakePortalAdapter { listed: vec![portal_shortcut("Ctrl+Shift+1")], @@ -780,6 +891,7 @@ mod tests { } #[tokio::test] + /// Verifies that a missing shortcut is bound with its converted preferred trigger. async fn portal_adapter_binds_missing_shortcut_with_preferred_trigger() { let mut adapter = FakePortalAdapter { bound: vec![portal_shortcut("Ctrl+Shift+1")], @@ -794,6 +906,7 @@ mod tests { } #[tokio::test] + /// Verifies that deliberate changes bind again instead of restoring the old trigger. async fn portal_adapter_rebinds_after_deliberate_change() { let mut adapter = FakePortalAdapter { listed: vec![portal_shortcut("Ctrl+Shift+1")], @@ -809,6 +922,7 @@ mod tests { } #[tokio::test] + /// Verifies that portal cancellation remains distinguishable from technical errors. async fn portal_adapter_preserves_cancellation() { let mut adapter = FakePortalAdapter { bind_failure: Some(PortalFailure { @@ -825,6 +939,7 @@ mod tests { } #[test] + /// Verifies that fallback is restricted to unavailable portals and safe active states. fn fallback_is_limited_to_an_unavailable_portal() { assert!(may_fallback_to_tauri(PortalFailureKind::Unavailable, None)); assert!(may_fallback_to_tauri(PortalFailureKind::Unavailable, Some(ShortcutBackend::Tauri))); @@ -835,6 +950,7 @@ mod tests { } #[test] + /// Verifies that suspend keeps portal sessions while unregistering Tauri bindings. fn suspend_keeps_portal_session_registered() { assert!(!unregister_backend_during_suspend(ShortcutBackend::Portal)); assert!(unregister_backend_during_suspend(ShortcutBackend::Tauri)); @@ -842,6 +958,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] + /// Verifies recognition of unavailable-portal D-Bus errors without misclassifying rejection. fn only_unavailable_portal_errors_allow_fallback() { assert!(portal_error_is_unavailable("org.freedesktop.DBus.Error.UnknownMethod")); assert!(portal_error_is_unavailable("ServiceUnknown"));