LocalKeyboardListener.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using ManagedCommon; 6 7 using Windows.System; 8 using Windows.Win32; 9 using Windows.Win32.Foundation; 10 using Windows.Win32.UI.WindowsAndMessaging; 11 12 namespace Microsoft.CmdPal.UI.Helpers; 13 14 /// <summary> 15 /// A class that listens for local keyboard events using a Windows hook. 16 /// </summary> 17 internal sealed partial class LocalKeyboardListener : IDisposable 18 { 19 /// <summary> 20 /// Event that is raised when a key is pressed down. 21 /// </summary> 22 public event EventHandler<LocalKeyboardListenerKeyPressedEventArgs>? KeyPressed; 23 24 private bool _disposed; 25 private UnhookWindowsHookExSafeHandle? _handle; 26 private HOOKPROC? _hookProc; // Keep reference to prevent GC collection 27 28 /// <summary> 29 /// Registers a global keyboard hook to listen for key down events. 30 /// </summary> 31 /// <exception cref="InvalidOperationException"> 32 /// Throws if the hook could not be registered, which may happen if the system is unable to set the hook. 33 /// </exception> 34 public void RegisterKeyboardHook() 35 { 36 ObjectDisposedException.ThrowIf(_disposed, this); 37 38 if (_handle is not null && !_handle.IsInvalid) 39 { 40 // Hook is already set 41 return; 42 } 43 44 _hookProc = KeyEventHook; 45 if (!SetWindowKeyHook(_hookProc)) 46 { 47 throw new InvalidOperationException("Failed to register keyboard hook."); 48 } 49 } 50 51 /// <summary> 52 /// Attempts to register a global keyboard hook to listen for key down events. 53 /// </summary> 54 /// <returns> 55 /// <see langword="true"/> if the keyboard hook was successfully registered; otherwise, <see langword="false"/>. 56 /// </returns> 57 public bool Start() 58 { 59 if (_disposed) 60 { 61 return false; 62 } 63 64 try 65 { 66 RegisterKeyboardHook(); 67 return true; 68 } 69 catch (Exception ex) 70 { 71 Logger.LogError("Failed to register hook", ex); 72 return false; 73 } 74 } 75 76 private void UnregisterKeyboardHook() 77 { 78 if (_handle is not null && !_handle.IsInvalid) 79 { 80 // The SafeHandle should automatically call UnhookWindowsHookEx when disposed 81 _handle.Dispose(); 82 _handle = null; 83 } 84 85 _hookProc = null; 86 } 87 88 private bool SetWindowKeyHook(HOOKPROC hookProc) 89 { 90 if (_handle is not null && !_handle.IsInvalid) 91 { 92 // Hook is already set 93 return false; 94 } 95 96 _handle = PInvoke.SetWindowsHookEx( 97 WINDOWS_HOOK_ID.WH_KEYBOARD, 98 hookProc, 99 PInvoke.GetModuleHandle(null), 100 PInvoke.GetCurrentThreadId()); 101 102 // Check if the hook was successfully set 103 return _handle is not null && !_handle.IsInvalid; 104 } 105 106 private static bool IsKeyDownHook(LPARAM lParam) 107 { 108 // The 30th bit tells what the previous key state is with 0 being the "UP" state 109 // For more info see https://learn.microsoft.com/windows/win32/winmsg/keyboardproc#lparam-in 110 return ((lParam.Value >> 30) & 1) == 0; 111 } 112 113 private LRESULT KeyEventHook(int nCode, WPARAM wParam, LPARAM lParam) 114 { 115 try 116 { 117 if (nCode >= 0 && IsKeyDownHook(lParam)) 118 { 119 InvokeKeyDown((VirtualKey)wParam.Value); 120 } 121 } 122 catch (Exception ex) 123 { 124 Logger.LogError("Failed when invoking key down keyboard hook event", ex); 125 } 126 127 // Call next hook in chain - pass null as first parameter for current hook 128 return PInvoke.CallNextHookEx(null, nCode, wParam, lParam); 129 } 130 131 private void InvokeKeyDown(VirtualKey virtualKey) 132 { 133 if (!_disposed) 134 { 135 KeyPressed?.Invoke(this, new LocalKeyboardListenerKeyPressedEventArgs(virtualKey)); 136 } 137 } 138 139 public void Dispose() 140 { 141 Dispose(true); 142 GC.SuppressFinalize(this); 143 } 144 145 private void Dispose(bool disposing) 146 { 147 if (!_disposed) 148 { 149 if (disposing) 150 { 151 UnregisterKeyboardHook(); 152 } 153 154 _disposed = true; 155 } 156 } 157 }