/ src / modules / cmdpal / Microsoft.CmdPal.UI / Helpers / LocalKeyboardListener.cs
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  }