diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
index c83369af..aaa600b7 100644
--- a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
+++ b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
@@ -1,4 +1,5 @@
using AIStudio.Dialogs;
+using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
@@ -33,7 +34,7 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
/// The name/identifier of the shortcut (used for conflict detection and registration).
///
[Parameter]
- public string ShortcutName { get; set; } = string.Empty;
+ public Shortcut ShortcutId { get; init; }
///
/// The icon to display.
@@ -80,7 +81,7 @@ public partial class ConfigurationShortcut : ConfigurationBaseCore
var dialogParameters = new DialogParameters
{
{ x => x.InitialShortcut, this.Shortcut() },
- { x => x.ShortcutName, this.ShortcutName },
+ { x => x.ShortcutId, this.ShortcutId },
};
var dialogReference = await this.DialogService.ShowAsync(
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
index 45fe4eea..62b996d0 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
@@ -1,5 +1,6 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
+@using AIStudio.Tools.Rust
@inherits SettingsPanelBase
@@ -33,6 +34,6 @@
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{
-
+
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
index d829c177..73a95e8d 100644
--- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
+++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
@@ -47,11 +47,12 @@ public partial class VoiceRecorder : MSGComponentBase
{
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")
+ if (tauriEvent.TryGetShortcut(out var shortcutId) && shortcutId == Shortcut.VOICE_RECORDING_TOGGLE)
{
this.Logger.LogInformation("Global shortcut triggered for voice recording toggle.");
await this.ToggleRecordingFromShortcut();
}
+
break;
}
}
diff --git a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
index 588140bb..2f772721 100644
--- a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
@@ -1,4 +1,5 @@
using AIStudio.Components;
+using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
@@ -24,10 +25,10 @@ public partial class ShortcutDialog : MSGComponentBase
public string InitialShortcut { get; set; } = string.Empty;
///
- /// The name/identifier of the shortcut for conflict detection.
+ /// The identifier of the shortcut for conflict detection.
///
[Parameter]
- public string ShortcutName { get; set; } = string.Empty;
+ public Shortcut ShortcutId { get; set; }
private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
diff --git a/app/MindWork AI Studio/Tools/Rust/Shortcut.cs b/app/MindWork AI Studio/Tools/Rust/Shortcut.cs
new file mode 100644
index 00000000..f8f783b3
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/Shortcut.cs
@@ -0,0 +1,17 @@
+namespace AIStudio.Tools.Rust;
+
+///
+/// Identifies a global keyboard shortcut.
+///
+public enum Shortcut
+{
+ ///
+ /// Null pattern - no shortcut assigned or unknown shortcut.
+ ///
+ NONE = 0,
+
+ ///
+ /// Toggles voice recording on/off.
+ ///
+ VOICE_RECORDING_TOGGLE,
+}
diff --git a/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs b/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
index c060e63a..c4cba654 100644
--- a/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
+++ b/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
@@ -5,4 +5,38 @@ namespace AIStudio.Tools.Rust;
///
/// The type of the Tauri event.
/// The payload of the Tauri event.
-public readonly record struct TauriEvent(TauriEventType EventType, List Payload);
\ No newline at end of file
+public readonly record struct TauriEvent(TauriEventType EventType, List Payload)
+{
+ ///
+ /// Attempts to parse the first payload element as a shortcut.
+ ///
+ /// The parsed shortcut name if successful.
+ /// True if parsing was successful, false otherwise.
+ public bool TryGetShortcut(out Shortcut shortcut)
+ {
+ shortcut = default;
+ if (this.Payload.Count == 0)
+ return false;
+
+ // Try standard enum parsing (handles PascalCase and numeric values):
+ if (Enum.TryParse(this.Payload[0], ignoreCase: true, out shortcut))
+ return true;
+
+ // Try parsing snake_case format (e.g., "voice_recording_toggle"):
+ return TryParseSnakeCase(this.Payload[0], out shortcut);
+ }
+
+ ///
+ /// Tries to parse a snake_case string into a ShortcutName enum value.
+ ///
+ private static bool TryParseSnakeCase(string value, out Shortcut shortcut)
+ {
+ shortcut = default;
+
+ // Convert snake_case to UPPER_SNAKE_CASE for enum matching:
+ var upperSnakeCase = value.ToUpperInvariant();
+
+ // Try to match against enum names (which are in UPPER_SNAKE_CASE):
+ return Enum.TryParse(upperSnakeCase, ignoreCase: false, out shortcut);
+ }
+};
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
index 23ddb553..e0865fbe 100644
--- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
+++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
@@ -1,5 +1,6 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;
@@ -57,44 +58,51 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
}
}
- public Task ProcessMessageWithResult(
- ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
- => Task.FromResult(default);
+ public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult(default);
#endregion
private async Task RegisterAllShortcuts()
{
this.logger.LogInformation("Registering global shortcuts.");
-
- //
- // Voice recording shortcut (preview feature)
- //
- if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager))
+ foreach (var shortcutId in Enum.GetValues())
{
- var shortcut = this.settingsManager.ConfigurationData.App.ShortcutVoiceRecording;
- if (!string.IsNullOrWhiteSpace(shortcut))
+ var shortcut = this.GetShortcutValue(shortcutId);
+ var isEnabled = this.IsShortcutAllowed(shortcutId);
+
+ if (isEnabled && !string.IsNullOrWhiteSpace(shortcut))
{
- var success = await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", shortcut);
+ var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut);
if (success)
- this.logger.LogInformation("Global shortcut 'voice_recording_toggle' ({Shortcut}) registered.", shortcut);
+ this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut);
else
- this.logger.LogWarning("Failed to register global shortcut 'voice_recording_toggle' ({Shortcut}).", shortcut);
+ this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut);
}
else
{
- // Disable shortcut when empty
- await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", string.Empty);
+ // Disable the shortcut when empty or feature is disabled:
+ await this.rustService.UpdateGlobalShortcut(shortcutId, string.Empty);
}
}
- else
- {
- // Disable the shortcut when the preview feature is disabled:
- await this.rustService.UpdateGlobalShortcut("voice_recording_toggle", string.Empty);
- }
-
+
this.logger.LogInformation("Global shortcuts registration completed.");
}
+ private string GetShortcutValue(Shortcut name) => name switch
+ {
+ Shortcut.VOICE_RECORDING_TOGGLE => this.settingsManager.ConfigurationData.App.ShortcutVoiceRecording,
+
+ _ => string.Empty,
+ };
+
+ private bool IsShortcutAllowed(Shortcut name) => name switch
+ {
+ // Voice recording is a preview feature:
+ Shortcut.VOICE_RECORDING_TOGGLE => PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.settingsManager),
+
+ // Other shortcuts are always allowed:
+ _ => true,
+ };
+
public static void Initialize() => IS_INITIALIZED = true;
}
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Shortcuts.cs b/app/MindWork AI Studio/Tools/Services/RustService.Shortcuts.cs
index 5da8d6ac..53cebaf7 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.Shortcuts.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Shortcuts.cs
@@ -1,4 +1,6 @@
// ReSharper disable NotAccessedPositionalProperty.Local
+using AIStudio.Tools.Rust;
+
namespace AIStudio.Tools.Services;
public sealed partial class RustService
@@ -6,35 +8,35 @@ public sealed partial class RustService
///
/// Registers or updates a global keyboard shortcut.
///
- /// The name/identifier for the shortcut (e.g., "voice_recording_toggle").
+ /// The identifier for the shortcut.
/// The shortcut string in Tauri format (e.g., "CmdOrControl+1"). Use empty string to disable.
/// True if the shortcut was registered successfully, false otherwise.
- public async Task UpdateGlobalShortcut(string name, string shortcut)
+ public async Task UpdateGlobalShortcut(Shortcut shortcutId, string shortcut)
{
try
{
- var request = new RegisterShortcutRequest(name, shortcut);
+ var request = new RegisterShortcutRequest(shortcutId, shortcut);
var response = await this.http.PostAsJsonAsync("/shortcuts/register", request, this.jsonRustSerializerOptions);
if (!response.IsSuccessStatusCode)
{
- this.logger?.LogError("Failed to register global shortcut '{Name}' due to network error: {StatusCode}", name, response.StatusCode);
+ this.logger?.LogError("Failed to register global shortcut '{ShortcutId}' due to network error: {StatusCode}", shortcutId, response.StatusCode);
return false;
}
var result = await response.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions);
if (result is null || !result.Success)
{
- this.logger?.LogError("Failed to register global shortcut '{Name}': {Error}", name, result?.ErrorMessage ?? "Unknown error");
+ this.logger?.LogError("Failed to register global shortcut '{ShortcutId}': {Error}", shortcutId, result?.ErrorMessage ?? "Unknown error");
return false;
}
- this.logger?.LogInformation("Global shortcut '{Name}' registered successfully with key '{Shortcut}'.", name, shortcut);
+ this.logger?.LogInformation("Global shortcut '{ShortcutId}' registered successfully with key '{Shortcut}'.", shortcutId, shortcut);
return true;
}
catch (Exception ex)
{
- this.logger?.LogError(ex, "Exception while registering global shortcut '{Name}'.", name);
+ this.logger?.LogError(ex, "Exception while registering global shortcut '{ShortcutId}'.", shortcutId);
return false;
}
}
@@ -136,7 +138,7 @@ public sealed partial class RustService
}
}
- private sealed record RegisterShortcutRequest(string Name, string Shortcut);
+ private sealed record RegisterShortcutRequest(Shortcut ShortcutId, string Shortcut);
private sealed record ShortcutResponse(bool Success, string ErrorMessage);
@@ -151,5 +153,5 @@ public sealed partial class RustService
/// Whether the shortcut syntax is valid.
/// Error message if not valid.
/// Whether the shortcut conflicts with another registered shortcut.
-/// Description of the conflict if any.
+/// Description of the conflict, if any.
public sealed record ShortcutValidationResult(bool IsValid, string ErrorMessage, bool HasConflict, string ConflictDescription);
diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs
index fe832090..b45ff00f 100644
--- a/runtime/src/app_window.rs
+++ b/runtime/src/app_window.rs
@@ -29,7 +29,24 @@ static CHECK_UPDATE_RESPONSE: Lazy>>> =
static EVENT_BROADCAST: Lazy>>> = Lazy::new(|| Mutex::new(None));
/// Stores the currently registered global shortcuts (name -> shortcut string).
-static REGISTERED_SHORTCUTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new()));
+static REGISTERED_SHORTCUTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new()));
+
+/// Enum identifying global keyboard shortcuts.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Shortcut {
+ None = 0,
+ VoiceRecordingToggle,
+}
+
+impl Shortcut {
+ /// Returns the display name for logging.
+ pub fn display_name(&self) -> &'static str {
+ match self {
+ Shortcut::None => "none",
+ Shortcut::VoiceRecordingToggle => "voice_recording_toggle",
+ }
+ }
+}
/// Starts the Tauri app.
pub fn start_tauri() {
@@ -686,8 +703,8 @@ pub struct FileSaveResponse {
/// Request payload for registering a global shortcut.
#[derive(Clone, Deserialize)]
pub struct RegisterShortcutRequest {
- /// The name/identifier for the shortcut (e.g., "voice_recording_toggle").
- name: String,
+ /// The shortcut ID to use.
+ id: Shortcut,
/// The shortcut string in Tauri format (e.g., "CmdOrControl+1").
/// Use empty string to unregister the shortcut.
@@ -707,17 +724,15 @@ pub struct ShortcutResponse {
fn register_shortcut_with_callback(
shortcut_manager: &mut impl tauri::GlobalShortcutManager,
shortcut: &str,
- name: &str,
+ shortcut_id: Shortcut,
event_sender: broadcast::Sender,
) -> Result<(), tauri::Error> {
- let shortcut_name = name.to_string();
-
//
// Match the shortcut registration to transform the Tauri result into the Rust result:
//
match shortcut_manager.register(shortcut, move || {
- info!(Source = "Tauri"; "Global shortcut triggered for '{shortcut_name}'.");
- let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_name.clone()]);
+ info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id.display_name());
+ let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]);
let sender = event_sender.clone();
tauri::async_runtime::spawn(async move {
match sender.send(event) {
@@ -735,10 +750,10 @@ fn register_shortcut_with_callback(
/// the existing shortcut for that name will be unregistered.
#[post("/shortcuts/register", data = "")]
pub fn register_shortcut(_token: APIToken, payload: Json) -> Json {
- let name = payload.name.clone();
+ let id = payload.id;
let new_shortcut = payload.shortcut.clone();
- info!(Source = "Tauri"; "Registering global shortcut '{name}' with key '{new_shortcut}'.");
+ info!(Source = "Tauri"; "Registering global shortcut '{}' with key '{new_shortcut}'.", id.display_name());
// Get the main window to access the global shortcut manager:
let main_window_lock = MAIN_WINDOW.lock().unwrap();
@@ -757,10 +772,10 @@ pub fn register_shortcut(_token: APIToken, payload: Json info!(Source = "Tauri"; "Unregistered old shortcut '{old_shortcut}' for '{name}'."),
+ Ok(_) => info!(Source = "Tauri"; "Unregistered old shortcut '{old_shortcut}' for '{}'.", id.display_name()),
Err(error) => warn!(Source = "Tauri"; "Failed to unregister old shortcut '{old_shortcut}': {error}"),
}
}
@@ -768,8 +783,8 @@ pub fn register_shortcut(_token: APIToken, payload: Json {
- info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{name}'.");
- registered_shortcuts.insert(name, new_shortcut);
+ info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id.display_name());
+ registered_shortcuts.insert(id, new_shortcut);
Json(ShortcutResponse {
success: true,
error_message: String::new(),
@@ -854,7 +869,7 @@ pub fn validate_shortcut(_token: APIToken, payload: Json Json {
for (name, shortcut) in registered_shortcuts.iter() {
if !shortcut.is_empty() {
match shortcut_manager.unregister(shortcut.as_str()) {
- Ok(_) => info!(Source = "Tauri"; "Temporarily unregistered shortcut '{shortcut}' for '{name}'."),
- Err(error) => warn!(Source = "Tauri"; "Failed to unregister shortcut '{shortcut}' for '{name}': {error}"),
+ Ok(_) => info!(Source = "Tauri"; "Temporarily unregistered shortcut '{shortcut}' for '{}'.", name.display_name()),
+ Err(error) => warn!(Source = "Tauri"; "Failed to unregister shortcut '{shortcut}' for '{}': {error}", name.display_name()),
}
}
}
@@ -958,18 +973,18 @@ pub fn resume_shortcuts(_token: APIToken) -> Json {
// Re-register all shortcuts with the OS:
let mut success_count = 0;
- for (name, shortcut) in registered_shortcuts.iter() {
+ for (shortcut_id, shortcut) in registered_shortcuts.iter() {
if shortcut.is_empty() {
continue;
}
- match register_shortcut_with_callback(&mut shortcut_manager, shortcut, name, event_sender.clone()) {
+ match register_shortcut_with_callback(&mut shortcut_manager, shortcut, *shortcut_id, event_sender.clone()) {
Ok(_) => {
- info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{name}'.");
+ info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id.display_name());
success_count += 1;
},
- Err(error) => warn!(Source = "Tauri"; "Failed to re-register shortcut '{shortcut}' for '{name}': {error}"),
+ Err(error) => warn!(Source = "Tauri"; "Failed to re-register shortcut '{shortcut}' for '{}': {error}", shortcut_id.display_name()),
}
}