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);
///
/// 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);
}