#![allow(non_snake_case)] // SPDX-License-Identifier: AGPL-3.0-or-later use std::collections::BTreeSet; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::mpsc::{self, SyncSender}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time::Duration; use napi::{ Env, Status, Task, bindgen_prelude::{AsyncTask, Function, Object, Result, ToNapiValue}, sys, threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode, UnknownReturnValue}, }; use napi_derive::napi; use x11rb::connection::{Connection, RequestConnection}; use x11rb::errors::ReplyError; use x11rb::protocol::Event; use x11rb::protocol::record::{ self, ConnectionExt as RecordConnectionExt, ExtRange, Range, Range8, Range16, }; use x11rb::protocol::xproto::{ ConnectionExt as XprotoConnectionExt, GetKeyboardMappingReply, Keycode, Mapping, }; use x11rb::rust_connection::RustConnection; use x11rb::wrapper::ConnectionExt as WrapperConnectionExt; use x11rb::x11_utils::TryParse; use crate::env::detect_display_server; use crate::keymap; use crate::modifiers::{self, Modifiers}; use crate::mouse; const RECORD_FROM_SERVER: u8 = 0; const RECORD_START_OF_DATA: u8 = 4; const KEY_PRESS: u8 = 2; const KEY_RELEASE: u8 = 3; const BUTTON_PRESS: u8 = 4; const BUTTON_RELEASE: u8 = 5; const START_TIMEOUT: Duration = Duration::from_secs(2); #[repr(C)] #[derive(Debug, Clone, Copy)] struct XKeyButtonProto { type_: u8, detail: u8, seq_l: u8, seq_h: u8, time: u32, root: u32, event: u32, child: u32, root_x: i16, root_y: i16, event_x: i16, event_y: i16, state: u16, same_screen: u8, pad0: u8, } #[derive(Debug, Clone)] pub enum EventKind { KeyDown, KeyUp, MouseDown, MouseUp, } impl EventKind { fn as_str(&self) -> &'static str { match self { Self::KeyDown => "keydown", Self::KeyUp => "keyup", Self::MouseDown => "mousedown", Self::MouseUp => "mouseup", } } } #[derive(Debug, Clone)] pub struct DecodedEvent { pub kind: EventKind, pub keycode: u32, pub x11_keycode: u8, pub key_name: String, pub button: u8, pub x: i32, pub y: i32, pub has_xy: bool, pub mods: Modifiers, } impl DecodedEvent { fn new(kind: EventKind, mods: Modifiers) -> Self { Self { kind, keycode: 0, x11_keycode: 0, key_name: String::new(), button: 0, x: 0, y: 0, has_xy: false, mods, } } } impl ToNapiValue for DecodedEvent { unsafe fn to_napi_value(raw_env: sys::napi_env, event: Self) -> Result { let env = Env::from_raw(raw_env); let mut object = Object::new(&env)?; object.set("type", event.kind.as_str())?; object.set("ctrlKey", event.mods.ctrl)?; object.set("altKey", event.mods.alt)?; object.set("shiftKey", event.mods.shift)?; object.set("metaKey", event.mods.meta)?; match event.kind { EventKind::KeyDown | EventKind::KeyUp => { object.set("keycode", event.keycode)?; object.set("x11Keycode", u32::from(event.x11_keycode))?; object.set("keyName", event.key_name.as_str())?; } EventKind::MouseDown | EventKind::MouseUp => { object.set("button", u32::from(event.button))?; if event.has_xy { object.set("x", event.x)?; object.set("y", event.y)?; } } } unsafe { as ToNapiValue>::to_napi_value(raw_env, object) } } } type EventTsfn = Arc>; #[derive(Clone)] struct KeysymCache { min_keycode: Keycode, syms: Vec, } impl KeysymCache { fn build(reply: &GetKeyboardMappingReply, min_keycode: Keycode) -> Self { let per = reply.keysyms_per_keycode as usize; let count = reply.keysyms.len().checked_div(per).unwrap_or(0); let mut syms = Vec::with_capacity(count); if per > 0 { for i in 0..count { syms.push(reply.keysyms[i * per]); } } Self { min_keycode, syms } } fn fetch(conn: &RustConnection) -> std::result::Result { let setup = conn.setup(); let min_keycode = setup.min_keycode; let count = setup .max_keycode .saturating_sub(min_keycode) .saturating_add(1); let mapping = conn .get_keyboard_mapping(min_keycode, count) .map_err(|err| format!("GetKeyboardMapping: {err}"))? .reply() .map_err(|err| format!("GetKeyboardMapping: {err}"))?; Ok(Self::build(&mapping, min_keycode)) } fn lookup(&self, keycode: u8) -> u32 { if keycode < self.min_keycode { return 0; } let idx = (keycode - self.min_keycode) as usize; self.syms.get(idx).copied().unwrap_or(0) } } struct Active { ctrl_conn: Arc, record_ctx: record::Context, worker: Option>, stop: Arc, } struct Inner { callback: EventTsfn, active: Mutex>, generation: AtomicU64, } impl Inner { fn lock(&self) -> Result>> { self.active .lock() .map_err(|_| generic_error("InputHook lock poisoned")) } fn start(&self, generation: u64) -> Result<()> { let dead = { let mut guard = self.lock()?; if guard .as_ref() .is_some_and(|active| !active.stop.load(Ordering::Acquire)) { return Ok(()); } guard.take() }; if let Some(dead) = dead { tear_down(dead); } let started = start_record(self.callback.clone())?; let mut guard = self.lock()?; if self.generation.load(Ordering::Acquire) != generation { drop(guard); tear_down(started); return Err(generic_error( "InputHook.start failed: stopped during start", )); } if guard .as_ref() .is_some_and(|active| !active.stop.load(Ordering::Acquire)) { drop(guard); tear_down(started); return Ok(()); } let replaced = guard.replace(started); drop(guard); if let Some(dead) = replaced { tear_down(dead); } Ok(()) } fn stop(&self) -> Result> { let mut guard = self.lock()?; self.generation.fetch_add(1, Ordering::AcqRel); Ok(guard.take()) } } pub struct StartTask { inner: Arc, generation: u64, } impl Task for StartTask { type Output = (); type JsValue = (); fn compute(&mut self) -> Result { self.inner.start(self.generation) } fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result { Ok(()) } } #[napi] pub struct InputHook { inner: Arc, } #[napi] impl InputHook { #[napi(constructor)] pub fn new(callback: Function) -> Result { let tsfn = callback .build_threadsafe_function::() .weak::() .callee_handled::() .build() .map_err(|err| generic_error(format!("failed to create TSFN: {}", err.reason)))?; Ok(Self { inner: Arc::new(Inner { callback: Arc::new(tsfn), active: Mutex::new(None), generation: AtomicU64::new(0), }), }) } #[napi] pub fn start(&self) -> AsyncTask { AsyncTask::new(StartTask { inner: self.inner.clone(), generation: self.inner.generation.load(Ordering::Acquire), }) } #[napi] pub fn stop(&self) -> Result<()> { if let Some(active) = self.inner.stop()? { tear_down(active); } Ok(()) } } impl Drop for InputHook { fn drop(&mut self) { if let Ok(Some(active)) = self.inner.stop() { tear_down(active); } } } fn start_record(callback: EventTsfn) -> Result { if !detect_display_server().supports_global_xrecord() { return Err(generic_error( "InputHook.start failed: WaylandUnsupported — global input \ capture is blocked by the Wayland security model. Use \ @fluxer/linux-evdev for kernel-level capture when the user has \ input device access." .to_string(), )); } let (ctrl_conn, _) = x11rb::connect(None) .map_err(|err| generic_error(format!("InputHook.start failed: NoXDisplay: {err}")))?; let (data_conn, _) = x11rb::connect(None) .map_err(|err| generic_error(format!("InputHook.start failed: NoXDisplay: {err}")))?; let ctrl_conn = Arc::new(ctrl_conn); let has_record = ctrl_conn .extension_information(record::X11_EXTENSION_NAME) .map_err(|err| generic_error(format!("InputHook.start failed: {err}")))? .is_some(); if !has_record { return Err(generic_error( "InputHook.start failed: RecordExtensionUnavailable", )); } ctrl_conn .record_query_version( record::X11_XML_VERSION.0 as _, record::X11_XML_VERSION.1 as _, ) .map_err(|err| generic_error(format!("InputHook.start failed: RecordQueryVersion: {err}")))? .reply() .map_err(|err| { generic_error(format!("InputHook.start failed: RecordQueryVersion: {err}")) })?; let keysyms = KeysymCache::fetch(&ctrl_conn) .map_err(|err| generic_error(format!("InputHook.start failed: {err}")))?; let record_ctx = ctrl_conn .generate_id() .map_err(|err| generic_error(format!("InputHook.start failed: GenerateId: {err}")))?; let empty = Range8 { first: 0, last: 0 }; let empty_ext = ExtRange { major: empty, minor: Range16 { first: 0, last: 0 }, }; let range = Range { core_requests: empty, core_replies: empty, ext_requests: empty_ext, ext_replies: empty_ext, delivered_events: empty, device_events: Range8 { first: KEY_PRESS, last: BUTTON_RELEASE, }, errors: empty, client_started: false, client_died: false, }; ctrl_conn .record_create_context(record_ctx, 0, &[record::CS::ALL_CLIENTS.into()], &[range]) .map_err(|err| { generic_error(format!( "InputHook.start failed: RecordCreateContext: {err}" )) })? .check() .map_err(|err| { generic_error(format!( "InputHook.start failed: RecordCreateContext: {err}" )) })?; let stop = Arc::new(AtomicBool::new(false)); let (ready_tx, ready_rx) = mpsc::sync_channel(1); let worker_ctrl = ctrl_conn.clone(); let worker_callback = callback.clone(); let worker_stop = stop.clone(); let worker = thread::Builder::new() .name("fluxer-linux-input-hook".to_string()) .spawn(move || { worker_main( &data_conn, Worker { ctrl_conn: worker_ctrl, record_ctx, keysyms, callback: worker_callback, stop: worker_stop, ready: Some(ready_tx), held: HeldInputs::default(), }, ); }); let worker = match worker { Ok(worker) => worker, Err(err) => { let _ = ctrl_conn.record_free_context(record_ctx); let _ = ctrl_conn.sync(); return Err(generic_error(format!( "InputHook.start failed: thread spawn: {err}" ))); } }; let active = Active { ctrl_conn, record_ctx, worker: Some(worker), stop, }; let failure = match ready_rx.recv_timeout(START_TIMEOUT) { Ok(Ok(())) => return Ok(active), Ok(Err(reason)) => reason, Err(mpsc::RecvTimeoutError::Timeout) => "RecordEnableContext timed out".to_string(), Err(mpsc::RecvTimeoutError::Disconnected) => { "RecordEnableContext worker exited".to_string() } }; abandon(active); Err(generic_error(format!("InputHook.start failed: {failure}"))) } struct Worker { ctrl_conn: Arc, record_ctx: record::Context, keysyms: KeysymCache, callback: EventTsfn, stop: Arc, ready: Option>>, held: HeldInputs, } #[derive(Default)] struct HeldInputs { keys: BTreeSet, buttons: BTreeSet, } impl HeldInputs { fn track(&mut self, event: &DecodedEvent) { match event.kind { EventKind::KeyDown => { self.keys.insert(event.x11_keycode); } EventKind::KeyUp => { self.keys.remove(&event.x11_keycode); } EventKind::MouseDown => { self.buttons.insert(event.button); } EventKind::MouseUp => { self.buttons.remove(&event.button); } } } fn take_releases(&mut self, keysyms: &KeysymCache) -> Vec { let mods = modifiers::from_state(0); let keys = std::mem::take(&mut self.keys) .into_iter() .map(|x11_keycode| key_event(EventKind::KeyUp, x11_keycode, keysyms, mods)); let buttons = std::mem::take(&mut self.buttons).into_iter().map(|button| { let mut event = DecodedEvent::new(EventKind::MouseUp, mods); event.button = button; event }); keys.chain(buttons).collect() } } impl Worker { fn report_ready(&mut self, result: std::result::Result<(), String>) { if let Some(ready) = self.ready.take() { let _ = ready.send(result); } } fn refresh_keysyms_on_mapping_change(&mut self, data_conn: &RustConnection) { let mut keyboard_changed = false; while let Ok(Some(event)) = data_conn.poll_for_event() { if let Event::MappingNotify(notify) = event && notify.request == Mapping::KEYBOARD { keyboard_changed = true; } } if !keyboard_changed { return; } if let Ok(keysyms) = KeysymCache::fetch(&self.ctrl_conn) { self.keysyms = keysyms; } while let Ok(Some(_)) = self.ctrl_conn.poll_for_event() {} } } fn worker_main(data_conn: &RustConnection, mut worker: Worker) { let cookie = match data_conn.record_enable_context(worker.record_ctx) { Ok(c) => c, Err(err) => { worker.stop.store(true, Ordering::Release); worker.report_ready(Err(format!("RecordEnableContext: {err}"))); return; } }; for reply in cookie { if worker.stop.load(Ordering::Acquire) { break; } let reply = match reply { Ok(r) => r, Err(err) if worker.ready.is_some() => { worker.report_ready(Err(format!("RecordEnableContext: {err}"))); break; } Err(ReplyError::ConnectionError(_)) => break, Err(_) => continue, }; if reply.category == RECORD_START_OF_DATA { worker.report_ready(Ok(())); continue; } if reply.client_swapped || reply.category != RECORD_FROM_SERVER { continue; } worker.refresh_keysyms_on_mapping_change(data_conn); let mut data: &[u8] = &reply.data; while !data.is_empty() { let (consumed, event) = decode_one(data, &worker.keysyms); if let Some(event) = event { worker.held.track(&event); dispatch(&worker.callback, &worker.stop, event); } if consumed == 0 || consumed > data.len() { break; } data = &data[consumed..]; } } if !worker.stop.swap(true, Ordering::AcqRel) { for event in worker.held.take_releases(&worker.keysyms) { dispatch(&worker.callback, &worker.stop, event); } } worker.report_ready(Err("RecordEnableContext ended before data".to_string())); } fn decode_one(data: &[u8], keysyms: &KeysymCache) -> (usize, Option) { if data.is_empty() { return (0, None); } match data[0] { KEY_PRESS | KEY_RELEASE | BUTTON_PRESS | BUTTON_RELEASE => { if data.len() < std::mem::size_of::() { return (0, None); } let evt: XKeyButtonProto = unsafe { std::ptr::read_unaligned(data.as_ptr() as *const XKeyButtonProto) }; (32, decode_event(&evt, keysyms)) } 0 => { if data.len() < 8 { return (0, None); } let (length, _) = match u32::try_parse(&data[4..]) { Ok(v) => v, Err(_) => return (0, None), }; (32 + (length as usize) * 4, None) } _ => (32, None), } } fn key_event( kind: EventKind, x11_keycode: u8, keysyms: &KeysymCache, mods: Modifiers, ) -> DecodedEvent { let keysym = keysyms.lookup(x11_keycode); let mut event = DecodedEvent::new(kind, mods); event.keycode = keysym; event.x11_keycode = x11_keycode; event.key_name = keymap::keysym_to_name(keysym) .unwrap_or_default() .to_string(); event } fn decode_event(evt: &XKeyButtonProto, keysyms: &KeysymCache) -> Option { let mods = modifiers::from_state(u32::from(evt.state)); match evt.type_ { KEY_PRESS => Some(key_event(EventKind::KeyDown, evt.detail, keysyms, mods)), KEY_RELEASE => Some(key_event(EventKind::KeyUp, evt.detail, keysyms, mods)), BUTTON_PRESS | BUTTON_RELEASE => { let button = mouse::browser_button(u32::from(evt.detail))?; let mut event = DecodedEvent::new( if evt.type_ == BUTTON_PRESS { EventKind::MouseDown } else { EventKind::MouseUp }, mods, ); event.button = button; event.x = i32::from(evt.root_x); event.y = i32::from(evt.root_y); event.has_xy = true; Some(event) } _ => None, } } fn dispatch(callback: &EventTsfn, stop: &Arc, event: DecodedEvent) { let status = callback.call(event, ThreadsafeFunctionCallMode::NonBlocking); if status == Status::Closing { stop.store(true, Ordering::Release); } } fn abandon(active: Active) { active.stop.store(true, Ordering::Release); let _ = active.ctrl_conn.record_disable_context(active.record_ctx); let _ = active.ctrl_conn.record_free_context(active.record_ctx); let _ = active.ctrl_conn.flush(); } fn tear_down(mut active: Active) { active.stop.store(true, Ordering::Release); if active.record_ctx != 0 { let _ = active.ctrl_conn.record_disable_context(active.record_ctx); let _ = active.ctrl_conn.sync(); } if let Some(worker) = active.worker.take() { let _ = worker.join(); } if active.record_ctx != 0 { let _ = active.ctrl_conn.record_free_context(active.record_ctx); let _ = active.ctrl_conn.sync(); } } #[napi(js_name = "isAvailable")] pub fn is_available() -> bool { if !detect_display_server().supports_global_xrecord() { return false; } x11rb::connect(None).is_ok() } fn generic_error(reason: impl Into) -> napi::Error { napi::Error::new(Status::GenericFailure, reason.into()) } #[allow(dead_code)] const _ASSERT_PROTO_LAYOUT: fn() = || { use std::mem::offset_of; let _ = offset_of!(XKeyButtonProto, seq_l); let _ = offset_of!(XKeyButtonProto, seq_h); let _ = offset_of!(XKeyButtonProto, event); let _ = offset_of!(XKeyButtonProto, child); let _ = offset_of!(XKeyButtonProto, event_x); let _ = offset_of!(XKeyButtonProto, event_y); let _ = offset_of!(XKeyButtonProto, same_screen); let _ = offset_of!(XKeyButtonProto, pad0); let _ = offset_of!(XKeyButtonProto, root); let _ = offset_of!(XKeyButtonProto, time); }; #[cfg(test)] mod tests { use super::*; fn cache_with(x11_keycode: u8, keysym: u32) -> KeysymCache { let mut syms = vec![0; 248]; syms[usize::from(x11_keycode - 8)] = keysym; KeysymCache { min_keycode: 8, syms, } } fn wire_event(type_: u8, detail: u8, state: u16) -> [u8; 32] { let mut data = [0u8; 32]; data[0] = type_; data[1] = detail; data[28..30].copy_from_slice(&state.to_ne_bytes()); data } #[test] fn decoded_keydown_has_keysym_raw_keycode_and_name() { let (consumed, event) = decode_one(&wire_event(KEY_PRESS, 38, 0), &cache_with(38, 0x0061)); let event = event.expect("key event"); assert_eq!(consumed, 32); assert!(matches!(event.kind, EventKind::KeyDown)); assert_eq!(event.keycode, 0x0061); assert_eq!(event.x11_keycode, 38); assert_eq!(event.key_name, "A"); assert!(!event.has_xy); } #[test] fn unknown_keysym_has_no_layout_key_name() { let (_, event) = decode_one(&wire_event(KEY_PRESS, 24, 0x4), &cache_with(24, 0x6ca)); let event = event.expect("key event"); assert_eq!(event.keycode, 0x6ca); assert_eq!(event.x11_keycode, 24); assert_eq!(event.key_name, ""); assert!(event.mods.ctrl); } #[test] fn wheel_buttons_are_not_decoded() { let cache = cache_with(38, 0x0061); assert!( decode_one(&wire_event(BUTTON_PRESS, 4, 0), &cache) .1 .is_none() ); let (_, event) = decode_one(&wire_event(BUTTON_PRESS, 8, 0), &cache); assert_eq!(event.expect("button event").button, 3); } #[test] fn held_inputs_release_every_pressed_key_and_button_once() { let cache = cache_with(38, 0x0061); let mut held = HeldInputs::default(); for data in [ wire_event(KEY_PRESS, 38, 0), wire_event(KEY_PRESS, 37, 0), wire_event(KEY_RELEASE, 37, 0), wire_event(BUTTON_PRESS, 9, 0), ] { held.track(&decode_one(&data, &cache).1.expect("event")); } let releases = held.take_releases(&cache); assert_eq!(releases.len(), 2); assert!(matches!(releases[0].kind, EventKind::KeyUp)); assert_eq!(releases[0].x11_keycode, 38); assert_eq!(releases[0].key_name, "A"); assert!(!releases[0].mods.ctrl); assert!(matches!(releases[1].kind, EventKind::MouseUp)); assert_eq!(releases[1].button, 4); assert!(held.take_releases(&cache).is_empty()); } #[test] fn x_key_button_proto_layout_offsets_match_x11_wire_format() { use std::mem::offset_of; assert_eq!(offset_of!(XKeyButtonProto, type_), 0); assert_eq!(offset_of!(XKeyButtonProto, detail), 1); assert_eq!(offset_of!(XKeyButtonProto, time), 4); assert_eq!(offset_of!(XKeyButtonProto, root_x), 20); assert_eq!(offset_of!(XKeyButtonProto, root_y), 22); assert_eq!(offset_of!(XKeyButtonProto, state), 28); } #[test] fn event_kind_to_string_matches_js_contract() { assert_eq!(EventKind::KeyDown.as_str(), "keydown"); assert_eq!(EventKind::KeyUp.as_str(), "keyup"); assert_eq!(EventKind::MouseDown.as_str(), "mousedown"); assert_eq!(EventKind::MouseUp.as_str(), "mouseup"); } #[test] fn keysym_cache_returns_zero_below_min_keycode() { let cache = KeysymCache { min_keycode: 8, syms: vec![0x61, 0x62, 0x63], }; assert_eq!(cache.lookup(7), 0); assert_eq!(cache.lookup(8), 0x61); assert_eq!(cache.lookup(10), 0x63); assert_eq!(cache.lookup(255), 0); } }