diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 5f0e15d8..598e7f52 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -1675,6 +1675,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T20906218
-- Use app default
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T3672477670"] = "Use app default"
+-- No shortcut configured
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T3099115336"] = "No shortcut configured"
+
+-- Change shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T4081853237"] = "Change shortcut"
+
+-- Configure Keyboard Shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T636303786"] = "Configure Keyboard Shortcut"
+
-- Yes, let the AI decide which data sources are needed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1031370894"] = "Yes, let the AI decide which data sources are needed."
@@ -2017,6 +2026,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
-- Do you want to show preview features in the app?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
+-- Voice recording shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"] = "Voice recording shortcut"
+
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"
@@ -2047,6 +2059,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
+-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."
+
-- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"
@@ -4612,6 +4627,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?"
+-- Save
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"
+
+-- Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1464973299"] = "Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused."
+
+-- Press a key combination...
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1468443151"] = "Press a key combination..."
+
+-- Clear Shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1807313248"] = "Clear Shortcut"
+
+-- Invalid shortcut: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T189893682"] = "Invalid shortcut: {0}"
+
+-- This shortcut conflicts with: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T2633102934"] = "This shortcut conflicts with: {0}"
+
+-- Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3060573513"] = "Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."
+
+-- Shortcut is valid and available.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3159532525"] = "Shortcut is valid and available."
+
+-- Define a shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3734850493"] = "Define a shortcut"
+
+-- This is the shortcut you previously used.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T4167229652"] = "This is the shortcut you previously used."
+
+-- Supported modifiers: Ctrl/Cmd, Shift, Alt.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T889258890"] = "Supported modifiers: Ctrl/Cmd, Shift, Alt."
+
+-- Cancel
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T900713019"] = "Cancel"
+
-- Please enter a value.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T3576780391"] = "Please enter a value."
@@ -5050,6 +5101,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
+-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems."
+
-- To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2644379659"] = "To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system."
diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor
index 9d8f0072..38409675 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor
@@ -48,7 +48,7 @@
OnAdornmentClick="() => this.SendMessage()"
Disabled="@this.IsInputForbidden()"
Immediate="@true"
- OnKeyUp="this.InputKeyEvent"
+ OnKeyUp="@this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Class="@this.UserInputClass"
Style="@this.UserInputStyle"/>
diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor
new file mode 100644
index 00000000..41f3b9a3
--- /dev/null
+++ b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor
@@ -0,0 +1,26 @@
+@inherits ConfigurationBaseCore
+
+
+
+
+ @if (string.IsNullOrWhiteSpace(this.Shortcut()))
+ {
+ @T("No shortcut configured")
+ }
+ else
+ {
+
+ @this.GetDisplayShortcut()
+
+ }
+
+
+ @T("Change shortcut")
+
+
diff --git a/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
new file mode 100644
index 00000000..aaa600b7
--- /dev/null
+++ b/app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
@@ -0,0 +1,109 @@
+using AIStudio.Dialogs;
+using AIStudio.Tools.Rust;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+using DialogOptions = AIStudio.Dialogs.DialogOptions;
+
+namespace AIStudio.Components;
+
+///
+/// A configuration component for capturing and displaying keyboard shortcuts.
+///
+public partial class ConfigurationShortcut : ConfigurationBaseCore
+{
+ [Inject]
+ private IDialogService DialogService { get; init; } = null!;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ ///
+ /// The current shortcut value.
+ ///
+ [Parameter]
+ public Func Shortcut { get; set; } = () => string.Empty;
+
+ ///
+ /// An action which is called when the shortcut was changed.
+ ///
+ [Parameter]
+ public Action ShortcutUpdate { get; set; } = _ => { };
+
+ ///
+ /// The name/identifier of the shortcut (used for conflict detection and registration).
+ ///
+ [Parameter]
+ public Shortcut ShortcutId { get; init; }
+
+ ///
+ /// The icon to display.
+ ///
+ [Parameter]
+ public string Icon { get; set; } = Icons.Material.Filled.Keyboard;
+
+ ///
+ /// The color of the icon.
+ ///
+ [Parameter]
+ public Color IconColor { get; set; } = Color.Default;
+
+ #region Overrides of ConfigurationBase
+
+ protected override bool Stretch => true;
+
+ protected override Variant Variant => Variant.Outlined;
+
+ protected override string Label => this.OptionDescription;
+
+ #endregion
+
+ private string GetDisplayShortcut()
+ {
+ var shortcut = this.Shortcut();
+ if (string.IsNullOrWhiteSpace(shortcut))
+ return string.Empty;
+
+ // Convert internal format to display format:
+ return shortcut
+ .Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
+ .Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
+ }
+
+ private async Task OpenDialog()
+ {
+ // Suspend shortcut processing while the dialog is open, so the user can
+ // press the current shortcut to re-enter it without triggering the action:
+ await this.RustService.SuspendShortcutProcessing();
+
+ try
+ {
+ var dialogParameters = new DialogParameters
+ {
+ { x => x.InitialShortcut, this.Shortcut() },
+ { x => x.ShortcutId, this.ShortcutId },
+ };
+
+ var dialogReference = await this.DialogService.ShowAsync(
+ this.T("Configure Keyboard Shortcut"),
+ dialogParameters,
+ DialogOptions.FULLSCREEN);
+
+ var dialogResult = await dialogReference.Result;
+ if (dialogResult is null || dialogResult.Canceled)
+ return;
+
+ if (dialogResult.Data is string newShortcut)
+ {
+ this.ShortcutUpdate(newShortcut);
+ await this.SettingsManager.StoreSettings();
+ await this.InformAboutChange();
+ }
+ }
+ finally
+ {
+ // Resume the shortcut processing when the dialog is closed:
+ await this.RustService.ResumeShortcutProcessing();
+ }
+ }
+}
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor
index 0a5c89c8..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,5 +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 8b58035b..73a95e8d 100644
--- a/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
+++ b/app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
@@ -1,5 +1,6 @@
using AIStudio.Provider;
using AIStudio.Tools.MIME;
+using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
@@ -24,6 +25,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 +41,38 @@ 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.TryGetShortcut(out var shortcutId) && shortcutId == Shortcut.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;
@@ -109,6 +145,10 @@ public partial class VoiceRecorder : MSGComponentBase
// Clean up the recording stream if starting failed:
await this.FinalizeRecordingStream();
}
+ finally
+ {
+ this.StateHasChanged();
+ }
}
else
{
diff --git a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor
new file mode 100644
index 00000000..eb90ed79
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor
@@ -0,0 +1,50 @@
+@inherits MSGComponentBase
+
+
+
+
+ @T("Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.")
+
+
+
+
+
+
+ @if (!string.IsNullOrWhiteSpace(this.validationMessage))
+ {
+
+ @this.validationMessage
+
+ }
+
+
+
+ @T("Clear Shortcut")
+
+
+
+ @T("Cancel")
+
+
+ @T("Save")
+
+
+
diff --git a/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
new file mode 100644
index 00000000..9809b818
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/ShortcutDialog.razor.cs
@@ -0,0 +1,385 @@
+using AIStudio.Components;
+using AIStudio.Tools.Rust;
+using AIStudio.Tools.Services;
+
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+
+namespace AIStudio.Dialogs;
+
+///
+/// A dialog for capturing and configuring keyboard shortcuts.
+///
+public partial class ShortcutDialog : MSGComponentBase
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Inject]
+ private RustService RustService { get; init; } = null!;
+
+ ///
+ /// The initial shortcut value (in internal format, e.g., "CmdOrControl+1").
+ ///
+ [Parameter]
+ public string InitialShortcut { get; set; } = string.Empty;
+
+ ///
+ /// The identifier of the shortcut for conflict detection.
+ ///
+ [Parameter]
+ public Shortcut ShortcutId { get; set; }
+
+ private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
+
+ private string currentShortcut = string.Empty;
+ private string originalShortcut = string.Empty;
+ private string validationMessage = string.Empty;
+ private Severity validationSeverity = Severity.Info;
+ private bool hasValidationError;
+
+ //
+ // Current key state:
+ //
+ private bool hasCtrl;
+ private bool hasShift;
+ private bool hasAlt;
+ private bool hasMeta;
+ private string? currentKey;
+ private MudTextField? inputField;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ await base.OnInitializedAsync();
+
+ // Configure the spellchecking for the user input:
+ this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
+
+ this.currentShortcut = this.InitialShortcut;
+ this.originalShortcut = this.InitialShortcut;
+ this.ParseExistingShortcut();
+ }
+
+ #endregion
+
+ private string ShowText => string.IsNullOrWhiteSpace(this.currentShortcut)
+ ? T("Press a key combination...")
+ : this.GetDisplayShortcut();
+
+ private void ParseExistingShortcut()
+ {
+ if (string.IsNullOrWhiteSpace(this.currentShortcut))
+ return;
+
+ // Parse the existing shortcut to set the state
+ var parts = this.currentShortcut.Split('+');
+ foreach (var part in parts)
+ {
+ switch (part.ToLowerInvariant())
+ {
+ case "cmdorcontrol":
+ case "commandorcontrol":
+ case "ctrl":
+ case "control":
+ case "cmd":
+ case "command":
+ this.hasCtrl = true;
+ break;
+
+ case "shift":
+ this.hasShift = true;
+ break;
+
+ case "alt":
+ this.hasAlt = true;
+ break;
+
+ case "meta":
+ case "super":
+ this.hasMeta = true;
+ break;
+
+ default:
+ this.currentKey = part;
+ break;
+ }
+ }
+ }
+
+ private async Task HandleKeyDown(KeyboardEventArgs e)
+ {
+ // Ignore pure modifier key presses:
+ if (IsModifierKey(e.Code))
+ {
+ this.UpdateModifiers(e);
+ this.currentKey = null;
+ this.UpdateShortcutString();
+ return;
+ }
+
+ this.UpdateModifiers(e);
+
+ // Get the key:
+ this.currentKey = TranslateKeyCode(e.Code);
+
+ // Validate: must have at least one modifier + a key
+ if (!this.hasCtrl && !this.hasShift && !this.hasAlt && !this.hasMeta)
+ {
+ this.validationMessage = T("Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).");
+ this.validationSeverity = Severity.Warning;
+ this.hasValidationError = true;
+ this.StateHasChanged();
+ return;
+ }
+
+ this.UpdateShortcutString();
+ await this.ValidateShortcut();
+
+ this.StateHasChanged();
+ }
+
+ private void UpdateModifiers(KeyboardEventArgs e)
+ {
+ this.hasCtrl = e.CtrlKey || e.MetaKey; // Treat Meta (Cmd on Mac) same as Ctrl for cross-platform
+ this.hasShift = e.ShiftKey;
+ this.hasAlt = e.AltKey;
+ this.hasMeta = e is { MetaKey: true, CtrlKey: false }; // Only set meta if not already using ctrl
+ }
+
+ private void UpdateShortcutString()
+ {
+ var parts = new List();
+
+ if (this.hasCtrl)
+ parts.Add("CmdOrControl");
+
+ if (this.hasShift)
+ parts.Add("Shift");
+
+ if (this.hasAlt)
+ parts.Add("Alt");
+
+ if (!string.IsNullOrWhiteSpace(this.currentKey))
+ parts.Add(this.currentKey);
+
+ this.currentShortcut = parts.Count > 0 ? string.Join("+", parts) : string.Empty;
+ this.StateHasChanged();
+ }
+
+ private async Task ValidateShortcut()
+ {
+ if (string.IsNullOrWhiteSpace(this.currentShortcut) || string.IsNullOrWhiteSpace(this.currentKey))
+ {
+ this.validationMessage = string.Empty;
+ this.hasValidationError = false;
+ return;
+ }
+
+ // Check if the shortcut is valid by trying to register it with Rust
+ var result = await this.RustService.ValidateShortcut(this.currentShortcut);
+ if (result.IsValid)
+ {
+ if (!string.IsNullOrWhiteSpace(this.originalShortcut)
+ && this.currentShortcut.Equals(this.originalShortcut, StringComparison.OrdinalIgnoreCase))
+ {
+ this.validationMessage = T("This is the shortcut you previously used.");
+ this.validationSeverity = Severity.Info;
+ this.hasValidationError = false;
+ this.StateHasChanged();
+ return;
+ }
+
+ if (result.HasConflict)
+ {
+ this.validationMessage = string.Format(T("This shortcut conflicts with: {0}"), result.ConflictDescription);
+ this.validationSeverity = Severity.Warning;
+ this.hasValidationError = false; // Allow saving, but warn
+ }
+ else
+ {
+ this.validationMessage = T("Shortcut is valid and available.");
+ this.validationSeverity = Severity.Success;
+ this.hasValidationError = false;
+ }
+ }
+ else
+ {
+ this.validationMessage = string.Format(T("Invalid shortcut: {0}"), result.ErrorMessage);
+ this.validationSeverity = Severity.Error;
+ this.hasValidationError = true;
+ }
+
+ this.StateHasChanged();
+ }
+
+ private string GetDisplayShortcut()
+ {
+ // Convert internal format to display format:
+ return this.currentShortcut
+ .Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
+ .Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
+ }
+
+ private void ClearShortcut()
+ {
+ this.currentShortcut = string.Empty;
+ this.currentKey = null;
+ this.hasCtrl = false;
+ this.hasShift = false;
+ this.hasAlt = false;
+ this.hasMeta = false;
+ this.validationMessage = string.Empty;
+ this.hasValidationError = false;
+ this.StateHasChanged();
+ }
+
+ private void Cancel() => this.MudDialog.Cancel();
+
+ private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.currentShortcut));
+
+ ///
+ /// Checks if the key code represents a modifier key.
+ ///
+ private static bool IsModifierKey(string code) => code switch
+ {
+ "ShiftLeft" or "ShiftRight" => true,
+ "ControlLeft" or "ControlRight" => true,
+ "AltLeft" or "AltRight" => true,
+ "MetaLeft" or "MetaRight" => true,
+
+ _ => false,
+ };
+
+ ///
+ /// Translates a JavaScript KeyboardEvent.code to Tauri shortcut format.
+ ///
+ private static string TranslateKeyCode(string code) => code switch
+ {
+ // Letters
+ "KeyA" => "A",
+ "KeyB" => "B",
+ "KeyC" => "C",
+ "KeyD" => "D",
+ "KeyE" => "E",
+ "KeyF" => "F",
+ "KeyG" => "G",
+ "KeyH" => "H",
+ "KeyI" => "I",
+ "KeyJ" => "J",
+ "KeyK" => "K",
+ "KeyL" => "L",
+ "KeyM" => "M",
+ "KeyN" => "N",
+ "KeyO" => "O",
+ "KeyP" => "P",
+ "KeyQ" => "Q",
+ "KeyR" => "R",
+ "KeyS" => "S",
+ "KeyT" => "T",
+ "KeyU" => "U",
+ "KeyV" => "V",
+ "KeyW" => "W",
+ "KeyX" => "X",
+ "KeyY" => "Y",
+ "KeyZ" => "Z",
+
+ // Numbers
+ "Digit0" => "0",
+ "Digit1" => "1",
+ "Digit2" => "2",
+ "Digit3" => "3",
+ "Digit4" => "4",
+ "Digit5" => "5",
+ "Digit6" => "6",
+ "Digit7" => "7",
+ "Digit8" => "8",
+ "Digit9" => "9",
+
+ // Function keys
+ "F1" => "F1",
+ "F2" => "F2",
+ "F3" => "F3",
+ "F4" => "F4",
+ "F5" => "F5",
+ "F6" => "F6",
+ "F7" => "F7",
+ "F8" => "F8",
+ "F9" => "F9",
+ "F10" => "F10",
+ "F11" => "F11",
+ "F12" => "F12",
+ "F13" => "F13",
+ "F14" => "F14",
+ "F15" => "F15",
+ "F16" => "F16",
+ "F17" => "F17",
+ "F18" => "F18",
+ "F19" => "F19",
+ "F20" => "F20",
+ "F21" => "F21",
+ "F22" => "F22",
+ "F23" => "F23",
+ "F24" => "F24",
+
+ // Special keys
+ "Space" => "Space",
+ "Enter" => "Enter",
+ "Tab" => "Tab",
+ "Escape" => "Escape",
+ "Backspace" => "Backspace",
+ "Delete" => "Delete",
+ "Insert" => "Insert",
+ "Home" => "Home",
+ "End" => "End",
+ "PageUp" => "PageUp",
+ "PageDown" => "PageDown",
+
+ // Arrow keys
+ "ArrowUp" => "Up",
+ "ArrowDown" => "Down",
+ "ArrowLeft" => "Left",
+ "ArrowRight" => "Right",
+
+ // Numpad
+ "Numpad0" => "Num0",
+ "Numpad1" => "Num1",
+ "Numpad2" => "Num2",
+ "Numpad3" => "Num3",
+ "Numpad4" => "Num4",
+ "Numpad5" => "Num5",
+ "Numpad6" => "Num6",
+ "Numpad7" => "Num7",
+ "Numpad8" => "Num8",
+ "Numpad9" => "Num9",
+ "NumpadAdd" => "NumAdd",
+ "NumpadSubtract" => "NumSubtract",
+ "NumpadMultiply" => "NumMultiply",
+ "NumpadDivide" => "NumDivide",
+ "NumpadDecimal" => "NumDecimal",
+ "NumpadEnter" => "NumEnter",
+
+ // Punctuation
+ "Minus" => "Minus",
+ "Equal" => "Equal",
+ "BracketLeft" => "BracketLeft",
+ "BracketRight" => "BracketRight",
+ "Backslash" => "Backslash",
+ "Semicolon" => "Semicolon",
+ "Quote" => "Quote",
+ "Backquote" => "Backquote",
+ "Comma" => "Comma",
+ "Period" => "Period",
+ "Slash" => "Slash",
+
+ // Default: return as-is
+ _ => code,
+ };
+
+ private void HandleBlur()
+ {
+ // Re-focus the input field to keep capturing keys:
+ this.inputField?.FocusAsync();
+ }
+}
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index fc89f248..4516b81b 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -97,6 +97,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
// Set the snackbar for the update service:
UpdateService.SetBlazorDependencies(this.Snackbar);
+ GlobalShortcutService.Initialize();
TemporaryChatService.Initialize();
// Should the navigation bar be open by default?
@@ -116,11 +117,6 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
await base.OnInitializedAsync();
}
- private void LoadNavItems()
- {
- this.navItems = new List(this.GetNavItems());
- }
-
#endregion
#region Implementation of ILang
@@ -251,6 +247,11 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
#endregion
+ private void LoadNavItems()
+ {
+ this.navItems = new List(this.GetNavItems());
+ }
+
private IEnumerable GetNavItems()
{
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor
index a348283f..13b7598d 100644
--- a/app/MindWork AI Studio/Pages/Information.razor
+++ b/app/MindWork AI Studio/Pages/Information.razor
@@ -196,6 +196,7 @@
+
@@ -224,4 +225,4 @@
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
index 9708c666..ff421ff7 100644
--- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
@@ -153,6 +153,16 @@ CONFIG["SETTINGS"] = {}
-- I18N_ASSISTANT
-- CONFIG["SETTINGS"]["DataApp.HiddenAssistants"] = { "ERI_ASSISTANT", "I18N_ASSISTANT" }
+-- Configure a global shortcut for starting and stopping dictation.
+--
+-- The format follows the Rust and Tauri conventions. Especially,
+-- when you want to use the CTRL key on Windows (or the CMD key on macOS),
+-- please use "CmdOrControl" as the key name. All parts of the shortcut
+-- must be separated by a plus sign (+).
+--
+-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
+-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
+
-- Example chat templates for this configuration:
CONFIG["CHAT_TEMPLATES"] = {}
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index a8906b2a..e7300566 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -1677,6 +1677,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T20906218
-- Use app default
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T3672477670"] = "App-Standard verwenden"
+-- No shortcut configured
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T3099115336"] = "Keinen Tastaturkurzbefehl konfiguriert"
+
+-- Change shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T4081853237"] = "Tastaturkurzbefehl ändern"
+
+-- Configure Keyboard Shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T636303786"] = "Tastaturkurzbefehl konfigurieren"
+
-- Yes, let the AI decide which data sources are needed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1031370894"] = "Ja, die KI soll entscheiden, welche Datenquellen benötigt werden."
@@ -2019,6 +2028,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
-- Do you want to show preview features in the app?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Möchten Sie Vorschaufunktionen in der App anzeigen lassen?"
+-- Voice recording shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"] = "Tastaturkurzbefehl für Sprachaufnahme"
+
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "Wie oft sollen wir nach App-Updates suchen?"
@@ -2049,6 +2061,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Wählen Sie die Sprache für die App aus."
+-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "Der globale Tastaturkurzbefehl zum Ein- und Ausschalten der Sprachaufnahme. Dieser Kurzbefehl funktioniert systemweit, auch wenn die App nicht im Vordergrund ist."
+
-- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Diktieren und Transkribieren deaktivieren"
@@ -4614,6 +4629,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Eines ihrer Profile vorauswählen?"
+-- Save
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Speichern"
+
+-- Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1464973299"] = "Drücken Sie die gewünschte Tastenkombination, um den Kurzbefehl festzulegen. Der Tastaturkurzbefehl wird global registriert und funktioniert auch, wenn die App nicht im Vordergrund ist."
+
+-- Press a key combination...
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1468443151"] = "Drücken Sie eine Tastenkombination …"
+
+-- Clear Shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1807313248"] = "Tastaturkurzbefehl löschen"
+
+-- Invalid shortcut: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T189893682"] = "Ungültige Tastenkombination: {0}"
+
+-- This shortcut conflicts with: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T2633102934"] = "Dieser Tastaturkurzbefehl steht in Konflikt mit: {0}"
+
+-- Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3060573513"] = "Bitte fügen Sie mindestens einen Modifikatortaste hinzu (Strg, Umschalt, Alt oder Cmd)."
+
+-- Shortcut is valid and available.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3159532525"] = "Der Tastaturkurzbefehl ist gültig und verfügbar."
+
+-- Define a shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3734850493"] = "Tastaturkurzbefehl festlegen"
+
+-- This is the shortcut you previously used.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T4167229652"] = "Dies ist der Tastaturkurzbefehl, den Sie zuvor verwendet haben."
+
+-- Supported modifiers: Ctrl/Cmd, Shift, Alt.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T889258890"] = "Unterstützte Modifikatortasten: Strg/Cmd, Umschalt, Alt."
+
+-- Cancel
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T900713019"] = "Abbrechen"
+
-- Please enter a value.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T3576780391"] = "Bitte geben Sie einen Wert ein."
@@ -5052,6 +5103,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Verwendete Open-
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit"
+-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "Dieses Crate stellt Derive-Makros für Rust-Enums bereit, die wir verwenden, um Boilerplate zu reduzieren, wenn wir String-Konvertierungen und Metadaten für Laufzeittypen implementieren. Das ist hilfreich für die Kommunikation zwischen unseren Rust- und .NET-Systemen."
+
-- To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2644379659"] = "Um die Antworten des LLM in anderen Apps nutzen zu können, verwenden wir häufig die Zwischenablage des jeweiligen Betriebssystems. Leider gibt es in .NET keine Lösung, die auf allen Betriebssystemen funktioniert. Deshalb habe ich mich für diese Bibliothek in Rust entschieden. So funktioniert die Datenübertragung zu anderen Apps auf jedem System."
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index ca447cfe..591f9860 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -1677,6 +1677,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T20906218
-- Use app default
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T3672477670"] = "Use app default"
+-- No shortcut configured
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T3099115336"] = "No shortcut configured"
+
+-- Change shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T4081853237"] = "Change shortcut"
+
+-- Configure Keyboard Shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T636303786"] = "Configure Keyboard Shortcut"
+
-- Yes, let the AI decide which data sources are needed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1031370894"] = "Yes, let the AI decide which data sources are needed."
@@ -2019,6 +2028,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
-- Do you want to show preview features in the app?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
+-- Voice recording shortcut
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"] = "Voice recording shortcut"
+
-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"
@@ -2049,6 +2061,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."
+-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."
+
-- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"
@@ -4614,6 +4629,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?"
+-- Save
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"
+
+-- Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1464973299"] = "Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused."
+
+-- Press a key combination...
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1468443151"] = "Press a key combination..."
+
+-- Clear Shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1807313248"] = "Clear Shortcut"
+
+-- Invalid shortcut: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T189893682"] = "Invalid shortcut: {0}"
+
+-- This shortcut conflicts with: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T2633102934"] = "This shortcut conflicts with: {0}"
+
+-- Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3060573513"] = "Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."
+
+-- Shortcut is valid and available.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3159532525"] = "Shortcut is valid and available."
+
+-- Define a shortcut
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3734850493"] = "Define a shortcut"
+
+-- This is the shortcut you previously used.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T4167229652"] = "This is the shortcut you previously used."
+
+-- Supported modifiers: Ctrl/Cmd, Shift, Alt.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T889258890"] = "Supported modifiers: Ctrl/Cmd, Shift, Alt."
+
+-- Cancel
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T900713019"] = "Cancel"
+
-- Please enter a value.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T3576780391"] = "Please enter a value."
@@ -5052,6 +5103,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source
-- Build time
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time"
+-- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.
+UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems."
+
-- To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2644379659"] = "To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system."
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index e87dc0ca..c577b2f4 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -133,6 +133,7 @@ internal sealed class Program
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+ builder.Services.AddHostedService();
// ReSharper disable AccessToDisposedClosure
builder.Services.AddHostedService(_ => rust);
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
index d2ac4bb7..fe4409de 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
@@ -82,6 +82,13 @@ public sealed class DataApp(Expression>? configSelection = n
///
public string UseTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UseTranscriptionProvider, string.Empty);
+ ///
+ /// The global keyboard shortcut for toggling voice recording.
+ /// Uses Tauri's shortcut format, e.g., "CmdOrControl+1" (Cmd+1 on macOS, Ctrl+1 on Windows/Linux).
+ /// Set to empty string to disable the global shortcut.
+ ///
+ public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
+
///
/// Should the user be allowed to add providers?
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
index 814aa971..e85c8eba 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
@@ -70,6 +70,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: hide some assistants?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun);
+ // Config: global voice recording shortcut
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShortcutVoiceRecording, this.Id, settingsTable, dryRun);
+
// Handle configured LLM providers:
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun);
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs
index 63eb3df5..bdfdba81 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs
@@ -181,6 +181,10 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.HiddenAssistants, AVAILABLE_PLUGINS))
wasConfigurationChanged = true;
+ // Check for the voice recording shortcut:
+ if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShortcutVoiceRecording, AVAILABLE_PLUGINS))
+ wasConfigurationChanged = true;
+
if (wasConfigurationChanged)
{
await SETTINGS_MANAGER.StoreSettings();
diff --git a/app/MindWork AI Studio/Tools/Rust/RegisterShortcutRequest.cs b/app/MindWork AI Studio/Tools/Rust/RegisterShortcutRequest.cs
new file mode 100644
index 00000000..d6d480ca
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/RegisterShortcutRequest.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Tools.Rust;
+
+public sealed record RegisterShortcutRequest(Shortcut Id, string Shortcut);
\ No newline at end of file
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/ShortcutResponse.cs b/app/MindWork AI Studio/Tools/Rust/ShortcutResponse.cs
new file mode 100644
index 00000000..1028d475
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/ShortcutResponse.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Tools.Rust;
+
+public sealed record ShortcutResponse(bool Success, string ErrorMessage);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResponse.cs b/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResponse.cs
new file mode 100644
index 00000000..3a4a3270
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResponse.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Tools.Rust;
+
+public sealed record ShortcutValidationResponse(bool IsValid, string ErrorMessage, bool HasConflict, string ConflictDescription);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResult.cs b/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResult.cs
new file mode 100644
index 00000000..7b482276
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/ShortcutValidationResult.cs
@@ -0,0 +1,10 @@
+namespace AIStudio.Tools.Rust;
+
+///
+/// Result of validating a keyboard shortcut.
+///
+/// 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.
+public sealed record ShortcutValidationResult(bool IsValid, string ErrorMessage, bool HasConflict, string ConflictDescription);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs b/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
index c060e63a..3e537a2d 100644
--- a/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
+++ b/app/MindWork AI Studio/Tools/Rust/TauriEvent.cs
@@ -5,4 +5,41 @@ 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.EventType != TauriEventType.GLOBAL_SHORTCUT_PRESSED)
+ return false;
+
+ 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/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/app/MindWork AI Studio/Tools/Rust/ValidateShortcutRequest.cs b/app/MindWork AI Studio/Tools/Rust/ValidateShortcutRequest.cs
new file mode 100644
index 00000000..2c045c67
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Rust/ValidateShortcutRequest.cs
@@ -0,0 +1,3 @@
+namespace AIStudio.Tools.Rust;
+
+public sealed record ValidateShortcutRequest(string 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
new file mode 100644
index 00000000..4515b78b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs
@@ -0,0 +1,111 @@
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.Rust;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Tools.Services;
+
+public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiver
+{
+ private static bool IS_INITIALIZED;
+
+ private readonly ILogger logger;
+ private readonly SettingsManager settingsManager;
+ private readonly MessageBus messageBus;
+ private readonly RustService rustService;
+
+ public GlobalShortcutService(
+ ILogger logger,
+ SettingsManager settingsManager,
+ MessageBus messageBus,
+ RustService rustService)
+ {
+ this.logger = logger;
+ this.settingsManager = settingsManager;
+ this.messageBus = messageBus;
+ this.rustService = rustService;
+
+ this.messageBus.RegisterComponent(this);
+ this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ // Wait until the app is fully initialized:
+ while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
+ await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
+
+ // Register shortcuts on startup:
+ await this.RegisterAllShortcuts();
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ this.messageBus.Unregister(this);
+ await base.StopAsync(cancellationToken);
+ }
+
+ #region IMessageBusReceiver
+
+ public async Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
+ {
+ switch (triggeredEvent)
+ {
+ case Event.CONFIGURATION_CHANGED:
+ await this.RegisterAllShortcuts();
+ break;
+ }
+ }
+
+ public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult(default);
+
+ #endregion
+
+ private async Task RegisterAllShortcuts()
+ {
+ this.logger.LogInformation("Registering global shortcuts.");
+ foreach (var shortcutId in Enum.GetValues())
+ {
+ if(shortcutId is Shortcut.NONE)
+ continue;
+
+ var shortcut = this.GetShortcutValue(shortcutId);
+ var isEnabled = this.IsShortcutAllowed(shortcutId);
+
+ if (isEnabled && !string.IsNullOrWhiteSpace(shortcut))
+ {
+ var success = await this.rustService.UpdateGlobalShortcut(shortcutId, shortcut);
+ if (success)
+ this.logger.LogInformation("Global shortcut '{ShortcutId}' ({Shortcut}) registered.", shortcutId, shortcut);
+ else
+ this.logger.LogWarning("Failed to register global shortcut '{ShortcutId}' ({Shortcut}).", shortcutId, shortcut);
+ }
+ else
+ {
+ // Disable the shortcut when empty or feature is disabled:
+ await this.rustService.UpdateGlobalShortcut(shortcutId, 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/RustEnumConverter.cs b/app/MindWork AI Studio/Tools/Services/RustEnumConverter.cs
new file mode 100644
index 00000000..deb14ec6
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/RustEnumConverter.cs
@@ -0,0 +1,112 @@
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// Converts enum values for Rust communication.
+///
+///
+/// Rust expects PascalCase enum values (e.g., "VoiceRecordingToggle"),
+/// while .NET uses UPPER_SNAKE_CASE (e.g., "VOICE_RECORDING_TOGGLE").
+/// This converter handles the bidirectional conversion.
+///
+public sealed class RustEnumConverter : JsonConverter