using System.Runtime.InteropServices; namespace CursorLang.Core.Interop; /// /// A system keyboard hook (WH_KEYBOARD_LL): sees key presses in every application /// and can keep them from going any further. /// /// /// 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. /// internal sealed class LowLevelKeyboardHook : IDisposable { /// /// A key event handler. Returns true when the event must be swallowed — /// then the foreground application will not see it. /// 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; /// The event came from SendInput rather than from a real key press. 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; /// /// Installs the hook. Returns false when the system refuses. /// 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(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; } }