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