From 6ed9338d23d7815afa5ee5fb063acf0bc5190742 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 20 Jan 2026 10:54:07 +0100 Subject: [PATCH] Initial implementation of global shortcut --- .../Components/VoiceRecorder.razor.cs | 36 +++++++++++++++++++ .../Tools/Rust/TauriEventType.cs | 6 ++-- runtime/Cargo.toml | 2 +- runtime/src/app_window.rs | 25 ++++++++++++- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs index 8b58035b..8924c7bd 100644 --- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs +++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs @@ -1,5 +1,7 @@ using AIStudio.Provider; +using AIStudio.Tools; using AIStudio.Tools.MIME; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -24,6 +26,9 @@ public partial class VoiceRecorder : MSGComponentBase protected override async Task OnInitializedAsync() { + // Register for global shortcut events: + this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED]); + await base.OnInitializedAsync(); try @@ -37,6 +42,37 @@ public partial class VoiceRecorder : MSGComponentBase } } + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + switch (triggeredEvent) + { + case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.GLOBAL_SHORTCUT_PRESSED } tauriEvent: + // Check if this is the voice recording toggle shortcut: + if (tauriEvent.Payload.Count > 0 && tauriEvent.Payload[0] == "voice_recording_toggle") + { + this.Logger.LogInformation("Global shortcut triggered for voice recording toggle."); + await this.ToggleRecordingFromShortcut(); + } + break; + } + } + + /// + /// Toggles the recording state when triggered by a global shortcut. + /// + private async Task ToggleRecordingFromShortcut() + { + // Don't allow toggle if transcription is in progress or preparing: + if (this.isTranscribing || this.isPreparing) + { + this.Logger.LogDebug("Ignoring shortcut: transcription or preparation is in progress."); + return; + } + + // Toggle the recording state: + await this.OnRecordingToggled(!this.isRecording); + } + #endregion private uint numReceivedChunks; diff --git a/app/MindWork AI Studio/Tools/Rust/TauriEventType.cs b/app/MindWork AI Studio/Tools/Rust/TauriEventType.cs index 2cd1c792..52afd491 100644 --- a/app/MindWork AI Studio/Tools/Rust/TauriEventType.cs +++ b/app/MindWork AI Studio/Tools/Rust/TauriEventType.cs @@ -8,11 +8,13 @@ public enum TauriEventType NONE, PING, UNKNOWN, - + WINDOW_FOCUSED, WINDOW_NOT_FOCUSED, - + FILE_DROP_HOVERED, FILE_DROP_DROPPED, FILE_DROP_CANCELED, + + GLOBAL_SHORTCUT_PRESSED, } \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 251ee1ce..3da1bf2e 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -9,7 +9,7 @@ authors = ["Thorsten Sommer"] tauri-build = { version = "1.5", features = [] } [dependencies] -tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] } +tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 87d58fda..d3e0f466 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -8,7 +8,7 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use serde::Deserialize; use tauri::updater::UpdateResponse; -use tauri::{FileDropEvent, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent}; +use tauri::{FileDropEvent, GlobalShortcutManager, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent}; use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::sync::broadcast; use tokio::time; @@ -65,6 +65,9 @@ pub fn start_tauri() { // Get the main window: let window = app.get_window("main").expect("Failed to get main window."); + // Clone the event sender for the global shortcut handler before moving it into the window event closure: + let shortcut_event_sender = event_sender.clone(); + // Register a callback for window events, such as file drops. We have to use // this handler in addition to the app event handler, because file drop events // are only available in the window event handler (is a bug, cf. https://github.com/tauri-apps/tauri/issues/14338): @@ -83,6 +86,24 @@ pub fn start_tauri() { // Save the main window for later access: *MAIN_WINDOW.lock().unwrap() = Some(window); + // Register global shortcuts for voice recording toggle. + // CmdOrControl+1 will be Cmd+1 on macOS and Ctrl+1 on Windows/Linux: + let mut shortcut_manager = app.global_shortcut_manager(); + match shortcut_manager.register("CmdOrControl+1", move || { + info!(Source = "Tauri"; "Global shortcut CmdOrControl+1 triggered for voice recording toggle."); + let event = Event::new(TauriEventType::GlobalShortcutPressed, vec!["voice_recording_toggle".to_string()]); + let sender = shortcut_event_sender.clone(); + tauri::async_runtime::spawn(async move { + match sender.send(event) { + Ok(_) => {}, + Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"), + } + }); + }) { + Ok(_) => info!(Source = "Bootloader Tauri"; "Global shortcut CmdOrControl+1 registered successfully."), + Err(error) => error!(Source = "Bootloader Tauri"; "Failed to register global shortcut: {error}"), + } + info!(Source = "Bootloader Tauri"; "Setup is running."); let data_path = app.path_resolver().app_local_data_dir().unwrap(); let data_path = data_path.join("data"); @@ -323,6 +344,8 @@ pub enum TauriEventType { FileDropHovered, FileDropDropped, FileDropCanceled, + + GlobalShortcutPressed, } /// Changes the location of the main window to the given URL.