131 lines
4.3 KiB
C#
131 lines
4.3 KiB
C#
using System.Runtime.InteropServices;
|
|
|
|
namespace CursorLang.Core.Interop;
|
|
|
|
/// <summary>
|
|
/// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application
|
|
/// and can keep them from going any further.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Only the low-level hook is installed: a regular WH_KEYBOARD requires injecting a
|
|
/// DLL into other processes, which is impossible for managed code. The callback
|
|
/// arrives on the thread that installed the hook, and that thread must pump a message
|
|
/// loop — hence the requirement to install the hook from the user interface thread.
|
|
/// Returning control must not be delayed: once the system timeout expires, Windows
|
|
/// silently removes the hook.
|
|
/// </remarks>
|
|
internal sealed class LowLevelKeyboardHook : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// A key event handler. Returns <c>true</c> when the event must be swallowed —
|
|
/// then the foreground application will not see it.
|
|
/// </summary>
|
|
internal delegate bool KeyFilter(int virtualKey, bool isKeyDown);
|
|
|
|
private const int WhKeyboardLowLevel = 13;
|
|
private const int HcAction = 0;
|
|
|
|
private const int WmKeyDown = 0x0100;
|
|
private const int WmKeyUp = 0x0101;
|
|
private const int WmSysKeyDown = 0x0104;
|
|
private const int WmSysKeyUp = 0x0105;
|
|
|
|
/// <summary>The event came from SendInput rather than from a real key press.</summary>
|
|
private const uint LowLevelKeyHookFlagInjected = 0x10;
|
|
|
|
private readonly KeyFilter _filter;
|
|
|
|
// The delegate lives in a field not for convenience: the only reference to it is
|
|
// held by Win32, which the garbage collector knows nothing about, and without the
|
|
// field the hook stops working after a random amount of time
|
|
private readonly HookProc _callback;
|
|
|
|
private IntPtr _handle;
|
|
|
|
internal LowLevelKeyboardHook(KeyFilter filter)
|
|
{
|
|
_filter = filter;
|
|
_callback = OnHookEvent;
|
|
}
|
|
|
|
private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);
|
|
|
|
internal bool IsInstalled => _handle != IntPtr.Zero;
|
|
|
|
/// <summary>
|
|
/// Installs the hook. Returns <c>false</c> when the system refuses.
|
|
/// </summary>
|
|
internal bool Install()
|
|
{
|
|
if (_handle != IntPtr.Zero)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
_handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0);
|
|
return _handle != IntPtr.Zero;
|
|
}
|
|
|
|
internal void Uninstall()
|
|
{
|
|
if (_handle == IntPtr.Zero)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UnhookWindowsHookEx(_handle);
|
|
_handle = IntPtr.Zero;
|
|
}
|
|
|
|
public void Dispose() => Uninstall();
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
|
|
|
|
[DllImport("user32.dll", SetLastError = true)]
|
|
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam);
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern IntPtr GetModuleHandle(string? lpModuleName);
|
|
|
|
private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
if (code != HcAction)
|
|
{
|
|
return CallNextHookEx(_handle, code, wParam, lParam);
|
|
}
|
|
|
|
var message = (int)wParam;
|
|
bool isKeyDown = message is WmKeyDown or WmSysKeyDown;
|
|
bool isKeyUp = message is WmKeyUp or WmSysKeyUp;
|
|
|
|
var data = Marshal.PtrToStructure<KeyboardHookData>(lParam);
|
|
|
|
// Synthetic input comes from on-screen keyboards, text expanders and
|
|
// automation tools: overriding what they do is none of our business
|
|
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
|
|
|
|
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
|
|
{
|
|
// A non-zero result instead of CallNextHookEx breaks the chain: the event
|
|
// will reach neither the application nor the Windows caps-lock handler
|
|
return 1;
|
|
}
|
|
|
|
return CallNextHookEx(_handle, code, wParam, lParam);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct KeyboardHookData
|
|
{
|
|
public uint vkCode;
|
|
public uint scanCode;
|
|
public uint flags;
|
|
public uint time;
|
|
public IntPtr dwExtraInfo;
|
|
}
|
|
}
|