using System; using System.Threading; using System.Threading.Channels; namespace Recordingtest.Recorder; public sealed record RawEvent(long TimestampMs, string Kind, int X, int Y, uint Code, int WheelDelta, string? FocusedElementPath = null); /// /// Installs WH_KEYBOARD_LL and WH_MOUSE_LL hooks on a dedicated thread with its own message loop. /// Pushes RawEvent into a Channel for the main loop to consume. /// public sealed class LowLevelHook : IDisposable { private readonly Channel _channel; private Thread? _thread; private IntPtr _kbHook = IntPtr.Zero; private IntPtr _mouseHook = IntPtr.Zero; private NativeMethods.HookProc? _kbProc; private NativeMethods.HookProc? _mouseProc; private uint _threadId; private volatile bool _running; public LowLevelHook(Channel channel) { _channel = channel; } public ChannelReader Reader => _channel.Reader; public void Start() { if (_running) return; _running = true; var ready = new ManualResetEventSlim(false); _thread = new Thread(() => HookThreadMain(ready)) { IsBackground = true, Name = "LowLevelHookThread", }; _thread.SetApartmentState(ApartmentState.STA); _thread.Start(); ready.Wait(); } private void HookThreadMain(ManualResetEventSlim ready) { _threadId = GetCurrentThreadId(); _kbProc = KeyboardProc; _mouseProc = MouseProc; var hMod = NativeMethods.GetModuleHandle(null); _kbHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, _kbProc, hMod, 0); _mouseHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _mouseProc, hMod, 0); ready.Set(); while (_running && NativeMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0) { NativeMethods.TranslateMessage(ref msg); NativeMethods.DispatchMessage(ref msg); } if (_kbHook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_kbHook); if (_mouseHook != IntPtr.Zero) NativeMethods.UnhookWindowsHookEx(_mouseHook); _kbHook = IntPtr.Zero; _mouseHook = IntPtr.Zero; } private IntPtr KeyboardProc(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { var data = System.Runtime.InteropServices.Marshal.PtrToStructure(lParam); var msg = wParam.ToInt32(); var kind = msg switch { NativeMethods.WM_KEYDOWN or NativeMethods.WM_SYSKEYDOWN => "key_down", NativeMethods.WM_KEYUP or NativeMethods.WM_SYSKEYUP => "key_up", _ => "key", }; _channel.Writer.TryWrite(new RawEvent(NowMs(), kind, 0, 0, data.vkCode, 0)); } return NativeMethods.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); } private IntPtr MouseProc(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { var data = System.Runtime.InteropServices.Marshal.PtrToStructure(lParam); var msg = wParam.ToInt32(); var kind = msg switch { NativeMethods.WM_LBUTTONDOWN => "mouse_down_l", NativeMethods.WM_LBUTTONUP => "mouse_up_l", NativeMethods.WM_RBUTTONDOWN => "mouse_down_r", NativeMethods.WM_RBUTTONUP => "mouse_up_r", NativeMethods.WM_MBUTTONDOWN => "mouse_down_m", NativeMethods.WM_MBUTTONUP => "mouse_up_m", NativeMethods.WM_MOUSEWHEEL => "wheel", NativeMethods.WM_MOUSEMOVE => "move", _ => "mouse", }; int wheel = 0; if (msg == NativeMethods.WM_MOUSEWHEEL) { wheel = (short)((data.mouseData >> 16) & 0xFFFF); } _channel.Writer.TryWrite(new RawEvent(NowMs(), kind, data.pt.x, data.pt.y, 0, wheel)); } return NativeMethods.CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); } private static long NowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); [System.Runtime.InteropServices.DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); public void Dispose() { if (!_running) return; _running = false; if (_threadId != 0) { // Post a WM_QUIT to break GetMessage loop PostThreadMessage(_threadId, 0x0012 /* WM_QUIT */, IntPtr.Zero, IntPtr.Zero); } _thread?.Join(1000); _channel.Writer.TryComplete(); } [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam); }