diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index bc5ba04289..7e460dba2f 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -637,6 +637,7 @@ hmodule
hmonitor
homies
homljgmgpmcbpjbnjpfijnhipfkiclkd
+HOOKPROC
HORZRES
HORZSIZE
Hostbackdropbrush
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListener.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListener.cs
new file mode 100644
index 0000000000..c3dd7c28bf
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListener.cs
@@ -0,0 +1,157 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using ManagedCommon;
+
+using Windows.System;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.UI.WindowsAndMessaging;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+///
+/// A class that listens for local keyboard events using a Windows hook.
+///
+internal sealed class LocalKeyboardListener : IDisposable
+{
+ ///
+ /// Event that is raised when a key is pressed down.
+ ///
+ public event EventHandler? KeyPressed;
+
+ private bool _disposed;
+ private UnhookWindowsHookExSafeHandle? _handle;
+ private HOOKPROC? _hookProc; // Keep reference to prevent GC collection
+
+ ///
+ /// Registers a global keyboard hook to listen for key down events.
+ ///
+ ///
+ /// Throws if the hook could not be registered, which may happen if the system is unable to set the hook.
+ ///
+ public void RegisterKeyboardHook()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ if (_handle is not null && !_handle.IsInvalid)
+ {
+ // Hook is already set
+ return;
+ }
+
+ _hookProc = KeyEventHook;
+ if (!SetWindowKeyHook(_hookProc))
+ {
+ throw new InvalidOperationException("Failed to register keyboard hook.");
+ }
+ }
+
+ ///
+ /// Attempts to register a global keyboard hook to listen for key down events.
+ ///
+ ///
+ /// if the keyboard hook was successfully registered; otherwise, .
+ ///
+ public bool Start()
+ {
+ if (_disposed)
+ {
+ return false;
+ }
+
+ try
+ {
+ RegisterKeyboardHook();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to register hook", ex);
+ return false;
+ }
+ }
+
+ private void UnregisterKeyboardHook()
+ {
+ if (_handle is not null && !_handle.IsInvalid)
+ {
+ // The SafeHandle should automatically call UnhookWindowsHookEx when disposed
+ _handle.Dispose();
+ _handle = null;
+ }
+
+ _hookProc = null;
+ }
+
+ private bool SetWindowKeyHook(HOOKPROC hookProc)
+ {
+ if (_handle is not null && !_handle.IsInvalid)
+ {
+ // Hook is already set
+ return false;
+ }
+
+ _handle = PInvoke.SetWindowsHookEx(
+ WINDOWS_HOOK_ID.WH_KEYBOARD,
+ hookProc,
+ PInvoke.GetModuleHandle(null),
+ PInvoke.GetCurrentThreadId());
+
+ // Check if the hook was successfully set
+ return _handle is not null && !_handle.IsInvalid;
+ }
+
+ private static bool IsKeyDownHook(LPARAM lParam)
+ {
+ // The 30th bit tells what the previous key state is with 0 being the "UP" state
+ // For more info see https://learn.microsoft.com/windows/win32/winmsg/keyboardproc#lparam-in
+ return ((lParam.Value >> 30) & 1) == 0;
+ }
+
+ private LRESULT KeyEventHook(int nCode, WPARAM wParam, LPARAM lParam)
+ {
+ try
+ {
+ if (nCode >= 0 && IsKeyDownHook(lParam))
+ {
+ InvokeKeyDown((VirtualKey)wParam.Value);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed when invoking key down keyboard hook event", ex);
+ }
+
+ // Call next hook in chain - pass null as first parameter for current hook
+ return PInvoke.CallNextHookEx(null, nCode, wParam, lParam);
+ }
+
+ private void InvokeKeyDown(VirtualKey virtualKey)
+ {
+ if (!_disposed)
+ {
+ KeyPressed?.Invoke(this, new LocalKeyboardListenerKeyPressedEventArgs(virtualKey));
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ UnregisterKeyboardHook();
+ }
+
+ _disposed = true;
+ }
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListenerKeyPressedEventArgs.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListenerKeyPressedEventArgs.cs
new file mode 100644
index 0000000000..ce26cd037f
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/LocalKeyboardListenerKeyPressedEventArgs.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Windows.System;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+public class LocalKeyboardListenerKeyPressedEventArgs(VirtualKey key) : EventArgs
+{
+ public VirtualKey Key { get; } = key;
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
index 012ec3ccf6..5311f4b8b4 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -27,6 +27,7 @@ using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
+using Windows.System;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
@@ -44,7 +45,8 @@ public sealed partial class MainWindow : WindowEx,
IRecipient,
IRecipient,
IRecipient,
- IRecipient
+ IRecipient,
+ IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
@@ -54,6 +56,7 @@ public sealed partial class MainWindow : WindowEx,
private readonly WNDPROC? _originalWndProc;
private readonly List _hotkeys = [];
private readonly KeyboardListener _keyboardListener;
+ private readonly LocalKeyboardListener _localKeyboardListener;
private bool _ignoreHotKeyWhenFullScreen = true;
private DesktopAcrylicController? _acrylicController;
@@ -116,6 +119,18 @@ public sealed partial class MainWindow : WindowEx,
{
Summon(string.Empty);
});
+
+ _localKeyboardListener = new LocalKeyboardListener();
+ _localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed;
+ _localKeyboardListener.Start();
+ }
+
+ private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
+ {
+ if (e.Key == VirtualKey.GoBack)
+ {
+ WeakReferenceMessenger.Default.Send(new GoBackMessage());
+ }
}
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
@@ -376,6 +391,7 @@ public sealed partial class MainWindow : WindowEx,
// WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592).
// Workaround by turning it off before shutdown.
App.Current.DebugSettings.FailFastOnErrors = false;
+ _localKeyboardListener.Dispose();
DisposeAcrylic();
_keyboardListener.Stop();
@@ -682,4 +698,10 @@ public sealed partial class MainWindow : WindowEx,
return PInvoke.CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
+
+ public void Dispose()
+ {
+ _localKeyboardListener.Dispose();
+ DisposeAcrylic();
+ }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt
index a432d9a808..aa31c5a3f2 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt
@@ -47,4 +47,10 @@ DWM_CLOAKED_APP
CoWaitForMultipleObjects
INFINITE
-CWMO_FLAGS
\ No newline at end of file
+CWMO_FLAGS
+
+GetCurrentThreadId
+SetWindowsHookEx
+UnhookWindowsHookEx
+CallNextHookEx
+GetModuleHandle
\ No newline at end of file