using System.Runtime.InteropServices; namespace VoiceCat.App.Native; /// /// Thin wrapper over the Win32 Raw Input API (user32) used to observe the push-to-talk key /// system-wide — i.e. while another application is focused. /// /// We deliberately use Raw Input (RegisterRawInputDevices + WM_INPUT with /// RIDEV_INPUTSINK) rather than a low-level keyboard hook (SetWindowsHookEx / /// WH_KEYBOARD_LL). A low-level hook is the textbook keylogger pattern and is exactly /// what antivirus heuristics flag — especially for an unsigned, statically linked MinGW binary /// like ours. Raw Input involves no DLL injection and no global hook: the OS simply posts /// WM_INPUT messages to our own window's message queue (it is the same input path games /// use), so it does not trip those heuristics. It also passes keystrokes through to the focused /// app rather than swallowing them. /// /// Only the keyboard device is registered; filters to the single PTT /// virtual-key code one level up (MainForm). /// internal static partial class RawInput { // ── Constants ────────────────────────────────────────────────────────────────────────── public const int WM_INPUT = 0x00FF; private const uint RID_INPUT = 0x10000003; // GetRawInputData: get the raw data private const uint RIM_TYPEKEYBOARD = 1; // RAWINPUTHEADER.dwType for a keyboard private const uint RIDEV_INPUTSINK = 0x00000100; // receive input even when not in foreground private const uint RIDEV_REMOVE = 0x00000001; // stop receiving input from the device private const ushort RI_KEY_BREAK = 0x01; // RAWKEYBOARD.Flags bit set on key-up private const ushort HID_USAGE_PAGE_GENERIC = 0x01; private const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06; // ── Structs (must match the Win32 layout exactly) ────────────────────────────────────── [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTDEVICE { public ushort usUsagePage; public ushort usUsage; public uint dwFlags; public IntPtr hwndTarget; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTHEADER { public uint dwType; public uint dwSize; public IntPtr hDevice; public IntPtr wParam; } [StructLayout(LayoutKind.Sequential)] private struct RAWKEYBOARD { public ushort MakeCode; public ushort Flags; public ushort Reserved; public ushort VKey; public uint Message; public uint ExtraInformation; } // ── P/Invoke (source-generated via LibraryImport, matching VoiceCat.Interop) ──────────── [LibraryImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static partial bool RegisterRawInputDevices( ref RAWINPUTDEVICE pRawInputDevices, uint uiNumDevices, uint cbSize); [LibraryImport("user32.dll")] private static partial uint GetRawInputData( IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader); [LibraryImport("user32.dll")] private static partial short GetAsyncKeyState(int vKey); // ── Public helpers ───────────────────────────────────────────────────────────────────── /// Register a background keyboard sink targeting . With /// RIDEV_INPUTSINK the window receives WM_INPUT even when it is not in the foreground. public static bool RegisterKeyboardSink(IntPtr hwnd) { var rid = new RAWINPUTDEVICE { usUsagePage = HID_USAGE_PAGE_GENERIC, usUsage = HID_USAGE_GENERIC_KEYBOARD, dwFlags = RIDEV_INPUTSINK, hwndTarget = hwnd, }; return RegisterRawInputDevices(ref rid, 1, (uint)Marshal.SizeOf()); } /// Tear down the keyboard sink. For RIDEV_REMOVE the target handle must be NULL. public static bool UnregisterKeyboardSink() { var rid = new RAWINPUTDEVICE { usUsagePage = HID_USAGE_PAGE_GENERIC, usUsage = HID_USAGE_GENERIC_KEYBOARD, dwFlags = RIDEV_REMOVE, hwndTarget = IntPtr.Zero, }; return RegisterRawInputDevices(ref rid, 1, (uint)Marshal.SizeOf()); } /// Decode a WM_INPUT message's (the message's LParam). /// Returns false for anything that isn't a keyboard event. On success, /// is the virtual-key code and distinguishes a release from a press /// (auto-repeat arrives as repeated presses). public static bool TryParseKey(IntPtr hRawInput, out ushort vkey, out bool keyUp) { vkey = 0; keyUp = false; uint headerSize = (uint)Marshal.SizeOf(); uint size = 0; // First call (pData == NULL) returns 0 on success and fills `size` with the buffer length. if (GetRawInputData(hRawInput, RID_INPUT, IntPtr.Zero, ref size, headerSize) != 0 || size == 0) return false; IntPtr buf = Marshal.AllocHGlobal((int)size); try { if (GetRawInputData(hRawInput, RID_INPUT, buf, ref size, headerSize) != size) return false; var header = Marshal.PtrToStructure(buf); if (header.dwType != RIM_TYPEKEYBOARD) return false; // The keyboard payload immediately follows the (8-byte-aligned) header. var kb = Marshal.PtrToStructure(buf + (int)headerSize); vkey = kb.VKey; keyUp = (kb.Flags & RI_KEY_BREAK) != 0; return true; } finally { Marshal.FreeHGlobal(buf); } } /// True if the given virtual-key code is currently held down. Used as a watchdog to /// catch a missed key-up (e.g. across an RDP / lock-screen focus switch) so PTT can't stick. public static bool IsKeyDown(int vKey) => (GetAsyncKeyState(vKey) & 0x8000) != 0; }