Files
cursor-lang/CursorLang/Interop/LowLevelKeyboardHook.cs
T

131 lines
5.1 KiB
C#

using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех
/// приложениях и умеет не пропускать их дальше.
/// </summary>
/// <remarks>
/// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения
/// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов
/// приходит на тот поток, который поставил хук, и поток обязан крутить цикл
/// сообщений — отсюда требование ставить хук из потока пользовательского
/// интерфейса. Возврат управления затягивать нельзя: по истечении системного
/// тайм-аута Windows молча снимает хук.
/// </remarks>
internal sealed class LowLevelKeyboardHook : IDisposable
{
/// <summary>
/// Обработчик события клавиши. Возвращает <c>true</c>, если событие нужно
/// проглотить — тогда приложение переднего плана его не увидит.
/// </summary>
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;
/// <summary>Событие пришло не от живого нажатия, а из SendInput.</summary>
private const uint LowLevelKeyHookFlagInjected = 0x10;
private readonly KeyFilter _filter;
// Делегат живёт в поле не для удобства: ссылку на него держит только Win32,
// о которой сборщик мусора не знает, и без поля хук перестаёт работать
// через случайное время
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;
/// <summary>
/// Ставит хук. Возвращает <c>false</c>, если система отказала.
/// </summary>
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<KeyboardHookData>(lParam);
// Синтетический ввод присылают экранные клавиатуры, программы
// автозамены и средства автоматизации: подменять их работу не наше дело
bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0;
if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown))
{
// Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие
// не дойдёт ни до приложения, ни до обработчика регистра в Windows
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;
}
}