use crate::collector::types::{
KeyboardEvent, KeyboardEventType, MouseEvent, SensorEvent, ShortcutEvent, ShortcutType,
};
use crossbeam_channel::{bounded, Receiver, Sender};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use std::time::Duration;
use windows::Win32::Foundation::{CloseHandle, HWND, LPARAM, LRESULT, WPARAM};
use windows::Win32::System::ProcessStatus::GetModuleFileNameExW;
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
use windows::Win32::UI::Input::KeyboardAndMouse::GetKeyState;
use windows::Win32::UI::WindowsAndMessaging::{
CallNextHookEx, GetForegroundWindow, GetWindowThreadProcessId, PeekMessageW, SetWindowsHookExW,
UnhookWindowsHookEx, HHOOK, KBDLLHOOKSTRUCT, MSLLHOOKSTRUCT, PM_REMOVE, WH_KEYBOARD_LL,
WH_MOUSE_LL, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP,
WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_QUIT, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_SYSKEYDOWN, WM_SYSKEYUP,
};
#[derive(Debug, Clone)]
pub struct CollectorConfig {
pub capture_keyboard: bool,
pub capture_mouse: bool,
}
impl Default for CollectorConfig {
fn default() -> Self {
Self {
capture_keyboard: true,
capture_mouse: true,
}
}
}
pub struct WindowsCollector {
config: CollectorConfig,
sender: Sender<SensorEvent>,
receiver: Receiver<SensorEvent>,
running: Arc<AtomicBool>,
thread_handle: Option<JoinHandle<()>>,
}
impl WindowsCollector {
pub fn new(config: CollectorConfig) -> Self {
let (sender, receiver) = bounded(10_000);
Self {
config,
sender,
receiver,
running: Arc::new(AtomicBool::new(false)),
thread_handle: None,
}
}
pub fn start(&mut self) -> Result<(), CollectorError> {
if self.running.load(Ordering::SeqCst) {
return Err(CollectorError::AlreadyRunning);
}
self.running.store(true, Ordering::SeqCst);
let sender = self.sender.clone();
let running = self.running.clone();
let config = self.config.clone();
let handle = thread::spawn(move || {
if let Err(e) = run_hook_loop(sender, running.clone(), config) {
eprintln!("Hook loop error: {e:?}");
}
running.store(false, Ordering::SeqCst);
});
self.thread_handle = Some(handle);
Ok(())
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Some(handle) = self.thread_handle.take() {
let _ = handle.join();
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub fn receiver(&self) -> &Receiver<SensorEvent> {
&self.receiver
}
pub fn try_recv(&self) -> Option<SensorEvent> {
self.receiver.try_recv().ok()
}
}
impl Drop for WindowsCollector {
fn drop(&mut self) {
self.stop();
}
}
#[derive(Debug)]
pub enum CollectorError {
AlreadyRunning,
HookInstallationFailed,
}
impl std::fmt::Display for CollectorError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CollectorError::AlreadyRunning => write!(f, "Collector is already running"),
CollectorError::HookInstallationFailed => {
write!(f, "Failed to install Windows hook")
}
}
}
}
impl std::error::Error for CollectorError {}
thread_local! {
static EVENT_SENDER: std::cell::RefCell<Option<Sender<SensorEvent>>> = const { std::cell::RefCell::new(None) };
static LAST_MOUSE_X: std::cell::RefCell<i32> = const { std::cell::RefCell::new(0) };
static LAST_MOUSE_Y: std::cell::RefCell<i32> = const { std::cell::RefCell::new(0) };
}
pub(crate) fn classify_vk_code(vk_code: u32) -> KeyboardEventType {
const VK_BACK: u32 = 0x08;
const VK_TAB: u32 = 0x09;
const VK_RETURN: u32 = 0x0D;
const VK_SHIFT: u32 = 0x10;
const VK_CONTROL: u32 = 0x11;
const VK_MENU: u32 = 0x12; const VK_ESCAPE: u32 = 0x1B;
const VK_PRIOR: u32 = 0x21; const VK_NEXT: u32 = 0x22; const VK_END: u32 = 0x23;
const VK_HOME: u32 = 0x24;
const VK_LEFT: u32 = 0x25;
const VK_UP: u32 = 0x26;
const VK_RIGHT: u32 = 0x27;
const VK_DOWN: u32 = 0x28;
const VK_DELETE: u32 = 0x2E;
const VK_LWIN: u32 = 0x5B;
const VK_RWIN: u32 = 0x5C;
const VK_F1: u32 = 0x70;
const VK_F12: u32 = 0x7B;
const VK_LSHIFT: u32 = 0xA0;
const VK_RSHIFT: u32 = 0xA1;
const VK_LCONTROL: u32 = 0xA2;
const VK_RCONTROL: u32 = 0xA3;
const VK_LMENU: u32 = 0xA4;
const VK_RMENU: u32 = 0xA5;
match vk_code {
VK_LEFT | VK_RIGHT | VK_UP | VK_DOWN | VK_PRIOR | VK_NEXT | VK_HOME | VK_END => {
KeyboardEventType::NavigationKey
}
VK_BACK => KeyboardEventType::Backspace,
VK_DELETE => KeyboardEventType::Delete,
VK_RETURN => KeyboardEventType::Enter,
VK_TAB => KeyboardEventType::Tab,
VK_ESCAPE => KeyboardEventType::Escape,
VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN | VK_LSHIFT | VK_RSHIFT
| VK_LCONTROL | VK_RCONTROL | VK_LMENU | VK_RMENU => KeyboardEventType::ModifierKey,
vk if (VK_F1..=VK_F12).contains(&vk) => KeyboardEventType::FunctionKey,
_ => KeyboardEventType::TypingTap,
}
}
unsafe fn detect_shortcut(vk_code: u32) -> Option<ShortcutType> {
let ctrl_down = (GetKeyState(0x11) as u16) & 0x8000 != 0; if !ctrl_down {
return None;
}
let shift_down = (GetKeyState(0x10) as u16) & 0x8000 != 0;
const VK_C: u32 = 0x43;
const VK_V: u32 = 0x56;
const VK_X: u32 = 0x58;
const VK_Z: u32 = 0x5A;
const VK_A: u32 = 0x41;
const VK_S: u32 = 0x53;
match vk_code {
VK_C => Some(ShortcutType::Copy),
VK_V => Some(ShortcutType::Paste),
VK_X => Some(ShortcutType::Cut),
VK_Z if shift_down => Some(ShortcutType::Redo),
VK_Z => Some(ShortcutType::Undo),
VK_A => Some(ShortcutType::SelectAll),
VK_S => Some(ShortcutType::Save),
_ => None,
}
}
unsafe extern "system" fn keyboard_hook_proc(
n_code: i32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
if n_code >= 0 {
let kb_struct = &*(l_param.0 as *const KBDLLHOOKSTRUCT);
let w_param_u32 = w_param.0 as u32;
let vk_code = kb_struct.vkCode;
let is_key_down = matches!(w_param_u32, WM_KEYDOWN | WM_SYSKEYDOWN);
if matches!(
w_param_u32,
WM_KEYDOWN | WM_KEYUP | WM_SYSKEYDOWN | WM_SYSKEYUP
) {
let event = if is_key_down {
if let Some(shortcut_type) = detect_shortcut(vk_code) {
SensorEvent::Shortcut(ShortcutEvent {
timestamp: chrono::Utc::now(),
shortcut_type,
})
} else {
let event_type = classify_vk_code(vk_code);
SensorEvent::Keyboard(KeyboardEvent::with_type(true, event_type))
}
} else {
let event_type = classify_vk_code(vk_code);
SensorEvent::Keyboard(KeyboardEvent::with_type(false, event_type))
};
EVENT_SENDER.with(|sender| {
if let Some(ref s) = *sender.borrow() {
let _ = s.try_send(event);
}
});
}
}
CallNextHookEx(HHOOK::default(), n_code, w_param, l_param)
}
unsafe extern "system" fn mouse_hook_proc(
n_code: i32,
w_param: WPARAM,
l_param: LPARAM,
) -> LRESULT {
if n_code >= 0 {
let mouse_struct = &*(l_param.0 as *const MSLLHOOKSTRUCT);
let w_param_u32 = w_param.0 as u32;
let event = match w_param_u32 {
WM_MOUSEMOVE => {
let current_x = mouse_struct.pt.x;
let current_y = mouse_struct.pt.y;
let (delta_x, delta_y) = LAST_MOUSE_X.with(|last_x| {
LAST_MOUSE_Y.with(|last_y| {
let lx = *last_x.borrow();
let ly = *last_y.borrow();
*last_x.borrow_mut() = current_x;
*last_y.borrow_mut() = current_y;
if lx == 0 && ly == 0 {
(0.0, 0.0)
} else {
((current_x - lx) as f64, (current_y - ly) as f64)
}
})
});
if delta_x.abs() > 0.1 || delta_y.abs() > 0.1 {
Some(SensorEvent::Mouse(MouseEvent::movement(delta_x, delta_y)))
} else {
None
}
}
WM_LBUTTONDOWN => Some(SensorEvent::Mouse(MouseEvent::click(true))),
WM_RBUTTONDOWN => Some(SensorEvent::Mouse(MouseEvent::click(false))),
WM_MOUSEWHEEL => {
let wheel_delta = ((mouse_struct.mouseData >> 16) & 0xFFFF) as i16 as f64;
let delta_y = wheel_delta / 120.0;
Some(SensorEvent::Mouse(MouseEvent::scroll(0.0, delta_y)))
}
WM_MOUSEHWHEEL => {
let wheel_delta = ((mouse_struct.mouseData >> 16) & 0xFFFF) as i16 as f64;
let delta_x = wheel_delta / 120.0;
Some(SensorEvent::Mouse(MouseEvent::scroll(delta_x, 0.0)))
}
WM_LBUTTONUP | WM_RBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP => None,
_ => None,
};
if let Some(event) = event {
EVENT_SENDER.with(|sender| {
if let Some(ref s) = *sender.borrow() {
let _ = s.try_send(event);
}
});
}
}
CallNextHookEx(HHOOK::default(), n_code, w_param, l_param)
}
fn run_hook_loop(
sender: Sender<SensorEvent>,
running: Arc<AtomicBool>,
config: CollectorConfig,
) -> Result<(), CollectorError> {
EVENT_SENDER.with(|s| {
*s.borrow_mut() = Some(sender);
});
LAST_MOUSE_X.with(|x| *x.borrow_mut() = 0);
LAST_MOUSE_Y.with(|y| *y.borrow_mut() = 0);
unsafe {
let mut hooks: Vec<HHOOK> = Vec::new();
if config.capture_keyboard {
let kb_hook = SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook_proc), None, 0);
if kb_hook.is_err() {
for hook in hooks {
let _ = UnhookWindowsHookEx(hook);
}
return Err(CollectorError::HookInstallationFailed);
}
hooks.push(kb_hook.unwrap());
}
if config.capture_mouse {
let mouse_hook = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), None, 0);
if mouse_hook.is_err() {
for hook in hooks {
let _ = UnhookWindowsHookEx(hook);
}
return Err(CollectorError::HookInstallationFailed);
}
hooks.push(mouse_hook.unwrap());
}
let mut msg = windows::Win32::UI::WindowsAndMessaging::MSG::default();
while running.load(Ordering::SeqCst) {
let result = PeekMessageW(&mut msg, HWND::default(), 0, 0, PM_REMOVE);
if result.as_bool() {
if msg.message == WM_QUIT {
break;
}
} else {
std::thread::sleep(Duration::from_millis(10));
}
}
for hook in hooks {
let _ = UnhookWindowsHookEx(hook);
}
}
Ok(())
}
pub fn get_frontmost_app_id() -> Option<String> {
unsafe {
let hwnd = GetForegroundWindow();
if hwnd.0.is_null() {
return None;
}
let mut process_id: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
if process_id == 0 {
return None;
}
let handle = OpenProcess(
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
false,
process_id,
)
.ok()?;
let mut buffer = [0u16; 260]; let len = GetModuleFileNameExW(handle, None, &mut buffer);
let _ = CloseHandle(handle);
if len == 0 {
return None;
}
let full_path = String::from_utf16_lossy(&buffer[..len as usize]);
full_path.rsplit('\\').next().map(|s| s.to_string())
}
}
pub fn check_permission() -> bool {
unsafe {
let hook_result = SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook_proc), None, 0);
if let Ok(hook) = hook_result {
let _ = UnhookWindowsHookEx(hook);
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_collector_config_default() {
let config = CollectorConfig::default();
assert!(config.capture_keyboard);
assert!(config.capture_mouse);
}
#[test]
fn test_collector_creation() {
let collector = WindowsCollector::new(CollectorConfig::default());
assert!(!collector.is_running());
}
#[test]
fn test_collector_lifecycle() {
let collector = WindowsCollector::new(CollectorConfig::default());
assert!(!collector.is_running());
}
#[test]
fn test_classify_vk_code_navigation() {
assert_eq!(classify_vk_code(0x25), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x26), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x27), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x28), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x21), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x22), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x24), KeyboardEventType::NavigationKey); assert_eq!(classify_vk_code(0x23), KeyboardEventType::NavigationKey); }
#[test]
fn test_classify_vk_code_special_keys() {
assert_eq!(classify_vk_code(0x08), KeyboardEventType::Backspace);
assert_eq!(classify_vk_code(0x2E), KeyboardEventType::Delete);
assert_eq!(classify_vk_code(0x0D), KeyboardEventType::Enter);
assert_eq!(classify_vk_code(0x09), KeyboardEventType::Tab);
assert_eq!(classify_vk_code(0x1B), KeyboardEventType::Escape);
}
#[test]
fn test_classify_vk_code_modifiers() {
assert_eq!(classify_vk_code(0x10), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0x11), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0x12), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0x5B), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0x5C), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0xA0), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0xA1), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0xA2), KeyboardEventType::ModifierKey); assert_eq!(classify_vk_code(0xA3), KeyboardEventType::ModifierKey); }
#[test]
fn test_classify_vk_code_function_keys() {
assert_eq!(classify_vk_code(0x70), KeyboardEventType::FunctionKey); assert_eq!(classify_vk_code(0x71), KeyboardEventType::FunctionKey); assert_eq!(classify_vk_code(0x7B), KeyboardEventType::FunctionKey); }
#[test]
fn test_classify_vk_code_typing() {
assert_eq!(classify_vk_code(0x41), KeyboardEventType::TypingTap); assert_eq!(classify_vk_code(0x5A), KeyboardEventType::TypingTap); assert_eq!(classify_vk_code(0x30), KeyboardEventType::TypingTap); assert_eq!(classify_vk_code(0x39), KeyboardEventType::TypingTap); assert_eq!(classify_vk_code(0x20), KeyboardEventType::TypingTap); }
#[test]
fn test_get_frontmost_app_id() {
let app_id = get_frontmost_app_id();
if let Some(ref id) = app_id {
assert!(!id.is_empty());
}
}
}