mod keycodes;
use std::cell::RefCell;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use windows_sys::Win32::Foundation::{LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{VK_CONTROL, VK_MENU, VK_SHIFT};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, GetMessageW, HC_ACTION, HHOOK, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MSG,
PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, WH_KEYBOARD_LL, WM_KEYDOWN,
WM_KEYUP, WM_QUIT, WM_SYSKEYDOWN, WM_SYSKEYUP,
};
use self::keycodes::key_from_vk;
use crate::log;
use crate::{Error, Event, EventKind, Key, tap::TapBuilder};
const SCAN_LEFT_SHIFT: u32 = 0x2A;
const SCAN_RIGHT_SHIFT: u32 = 0x36;
thread_local! {
static THREAD_CTX: RefCell<Option<ThreadCtx>> = const { RefCell::new(None) };
}
struct ThreadCtx {
tx: Sender<Event>,
hook: HHOOK,
held: HashSet<u32>,
}
#[derive(Debug)]
pub(crate) struct ShutdownGuard {
thread_id: Arc<AtomicU32>,
thread: Option<JoinHandle<()>>,
_signaled: AtomicBool,
}
impl Drop for ShutdownGuard {
fn drop(&mut self) {
log::debug!("keytap: stopping Windows WH_KEYBOARD_LL hook");
let tid = self.thread_id.load(Ordering::Acquire);
if tid != 0 {
unsafe {
PostThreadMessageW(tid, WM_QUIT, 0, 0);
}
}
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
pub(crate) fn start(tx: Sender<Event>, _cfg: &TapBuilder) -> Result<ShutdownGuard, Error> {
log::debug!("keytap: starting Windows WH_KEYBOARD_LL hook");
let thread_id = Arc::new(AtomicU32::new(0));
let thread_id_worker = thread_id.clone();
let (ready_tx, ready_rx) = crossbeam_channel::bounded::<Result<(), Error>>(1);
let thread = thread::Builder::new()
.name("keytap-windows-ll-hook".into())
.spawn(move || {
let tid = unsafe { GetCurrentThreadId() };
thread_id_worker.store(tid, Ordering::Release);
let hook = unsafe {
SetWindowsHookExW(WH_KEYBOARD_LL, Some(raw_callback), std::ptr::null_mut(), 0)
};
if hook.is_null() {
let _ = ready_tx.send(Err(Error::TapFailed(
"SetWindowsHookExW returned NULL".into(),
)));
return;
}
THREAD_CTX.with(|cell| {
*cell.borrow_mut() = Some(ThreadCtx {
tx,
hook,
held: HashSet::new(),
});
});
let _ = ready_tx.send(Ok(()));
let mut msg: MSG = unsafe { std::mem::zeroed() };
loop {
let r = unsafe { GetMessageW(&mut msg, std::ptr::null_mut(), 0, 0) };
if r == 0 || r == -1 {
break;
}
}
unsafe {
UnhookWindowsHookEx(hook);
}
THREAD_CTX.with(|cell| {
cell.borrow_mut().take();
});
})
.map_err(|e| Error::TapFailed(format!("spawn LL hook thread: {e}")))?;
match ready_rx.recv_timeout(Duration::from_secs(2)) {
Ok(Ok(())) => Ok(ShutdownGuard {
thread_id,
thread: Some(thread),
_signaled: AtomicBool::new(false),
}),
Ok(Err(e)) => {
let _ = thread.join();
Err(e)
}
Err(_) => {
let tid = thread_id.load(Ordering::Acquire);
if tid != 0 {
unsafe {
PostThreadMessageW(tid, WM_QUIT, 0, 0);
}
}
let _ = thread.join();
Err(Error::TapFailed(
"LL hook install handshake timed out".into(),
))
}
}
}
unsafe extern "system" fn raw_callback(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
if code == HC_ACTION as i32 {
let raw: &KBDLLHOOKSTRUCT = unsafe { &*(lparam as *const KBDLLHOOKSTRUCT) };
let vk = raw.vkCode;
let scan = raw.scanCode;
let extended = (raw.flags & LLKHF_EXTENDED) != 0;
let key = resolve_key(vk, scan, extended);
let msg = wparam as u32;
let is_down = msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN;
let is_up = msg == WM_KEYUP || msg == WM_SYSKEYUP;
THREAD_CTX.with(|cell| {
if let Some(ctx) = cell.borrow_mut().as_mut() {
let kind = if is_down {
if !ctx.held.insert(vk) {
Some(EventKind::KeyRepeat(key))
} else {
Some(EventKind::KeyDown(key))
}
} else if is_up {
ctx.held.remove(&vk);
Some(EventKind::KeyUp(key))
} else {
None
};
if let Some(kind) = kind {
if ctx
.tx
.try_send(Event {
time: Instant::now(),
kind,
})
.is_err()
{
log::trace!("keytap: channel full — dropping event");
}
}
}
});
}
unsafe { CallNextHookEx(std::ptr::null_mut(), code, wparam, lparam) }
}
fn resolve_key(vk: u32, scan: u32, extended: bool) -> Key {
if vk == VK_SHIFT as u32 {
return match scan {
SCAN_RIGHT_SHIFT => Key::ShiftRight,
_ => Key::ShiftLeft,
};
}
if vk == VK_CONTROL as u32 {
return if extended {
Key::ControlRight
} else {
Key::ControlLeft
};
}
if vk == VK_MENU as u32 {
return if extended {
Key::AltRight
} else {
Key::AltLeft
};
}
key_from_vk(vk)
}