Added focused-window shortcut fallback on Linux (#882)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-07-21 14:43:33 +02:00
1 parent db508edc28
commit 1e5f07cb01
11 files changed
+350 -17

No files matched your search

@@ -8,4 +8,5 @@ public enum ShortcutBackend
NONE,
PORTAL,
TAURI,
LOCAL,
}
@@ -20,13 +20,19 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
}
private readonly SemaphoreSlim registrationSemaphore = new(1, 1);
private readonly object runtimeStateLock = new();
private readonly Dictionary<Shortcut, ShortcutState> lastSentStates = [];
private readonly Dictionary<Shortcut, string> lastNonEmptyShortcuts = [];
private readonly Dictionary<Shortcut, ShortcutRuntimeBinding> runtimeBindings = [];
private readonly ILogger<GlobalShortcutService> logger;
private readonly SettingsManager settingsManager;
private readonly MessageBus messageBus;
private readonly RustService rustService;
private readonly VoiceRecordingAvailabilityService voiceRecordingAvailabilityService;
private bool isProcessingSuspended;
private bool localFallbackWarningShown;
public event Func<GlobalShortcutRuntimeState, Task>? RuntimeStateChanged;
public GlobalShortcutService(
ILogger<GlobalShortcutService> logger,
@@ -58,6 +64,45 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
await base.StopAsync(cancellationToken);
}
/// <summary>
/// Returns the active backend and processing state for a shortcut.
/// </summary>
public GlobalShortcutRuntimeState GetRuntimeState(Shortcut shortcutId)
{
lock (this.runtimeStateLock)
{
if (this.runtimeBindings.TryGetValue(shortcutId, out var binding))
return new(shortcutId, binding.Shortcut, binding.Backend, this.isProcessingSuspended);
return new(shortcutId, string.Empty, ShortcutBackend.NONE, this.isProcessingSuspended);
}
}
/// <summary>
/// Pauses native and focused-window shortcut processing.
/// </summary>
public async Task<bool> SuspendShortcutProcessing()
{
lock (this.runtimeStateLock)
this.isProcessingSuspended = true;
await this.PublishAllRuntimeStates();
return await this.rustService.SuspendShortcutProcessing();
}
/// <summary>
/// Resumes native and focused-window shortcut processing.
/// </summary>
public async Task<bool> ResumeShortcutProcessing()
{
var result = await this.rustService.ResumeShortcutProcessing();
lock (this.runtimeStateLock)
this.isProcessingSuspended = false;
await this.PublishAllRuntimeStates();
return result;
}
#region IMessageBusReceiver
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
@@ -155,6 +200,9 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
if (!string.IsNullOrWhiteSpace(requestedState.Shortcut))
this.lastNonEmptyShortcuts[shortcutId] = requestedState.Shortcut;
lock (this.runtimeStateLock)
this.runtimeBindings[shortcutId] = new(requestedState.Shortcut, result.Backend);
this.logger.LogInformation(
"Global shortcut '{ShortcutId}' ({Shortcut}) synchronized through {Backend}.",
shortcutId,
@@ -163,6 +211,15 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
if (result.Backend is ShortcutBackend.PORTAL)
await this.UpdateEffectiveDisplayName(shortcutId, result.EffectiveDisplayName);
await this.PublishRuntimeState(shortcutId);
if (result.Backend is ShortcutBackend.LOCAL && !this.localFallbackWarningShown)
{
this.localFallbackWarningShown = true;
await this.messageBus.SendWarning(new(
Icons.Material.Filled.Keyboard,
TB("The voice recording shortcut currently works only while AI Studio is focused.")));
}
}
else
{
@@ -236,6 +293,31 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
await this.messageBus.SendMessage<bool>(null, Event.GLOBAL_SHORTCUT_CHANGED);
}
private async Task PublishAllRuntimeStates()
{
Shortcut[] shortcutIds;
lock (this.runtimeStateLock)
shortcutIds = this.runtimeBindings.Keys.ToArray();
foreach (var shortcutId in shortcutIds)
await this.PublishRuntimeState(shortcutId);
}
private async Task PublishRuntimeState(Shortcut shortcutId)
{
var subscribers = this.RuntimeStateChanged;
if (subscribers is null)
return;
var handlers = subscribers.GetInvocationList()
.Cast<Func<GlobalShortcutRuntimeState, Task>>()
.ToArray();
var runtimeState = this.GetRuntimeState(shortcutId);
foreach (var handler in handlers)
await handler(runtimeState);
}
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(GlobalShortcutService).Namespace, nameof(GlobalShortcutService));
private async Task<ShortcutState> GetShortcutState(Shortcut shortcutId, ShortcutSyncSource source)
@@ -262,4 +344,12 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
}
private readonly record struct ShortcutState(string Shortcut, bool IsEnabled, bool UsesPersistedFallback);
}
private readonly record struct ShortcutRuntimeBinding(string Shortcut, ShortcutBackend Backend);
}
public sealed record GlobalShortcutRuntimeState(
Shortcut ShortcutId,
string Shortcut,
ShortcutBackend Backend,
bool IsSuspended);