use std::collections::HashSet; use anyhow::{bail, Context, Result}; use evdev::{Device, InputEvent, KeyCode, EventType}; use tokio::sync::mpsc::Sender; use tokio::time::{interval, Duration}; use navka_common::navka::HidReport; use crate::input::keymap::{keycode_to_usage, is_modifier}; /// Up to 6 normal keys. Byte[0] = modifiers, Byte[1] = reserved, Byte[2..7] = pressed keys. /// Magic chord example: LeftCtrl + LeftAlt + LeftShift + Esc const MAGIC_CHORD: &[KeyCode] = &[ KeyCode::KEY_LEFTCTRL, KeyCode::KEY_LEFTSHIFT, KeyCode::KEY_LEFTALT, KeyCode::KEY_ESC, ]; pub struct KeyboardAggregator { // MPSC channel to server tx: Sender, // The list of evdev devices we are reading devices: Vec, // Pressed keys for building HID reports pressed_keys: HashSet, } impl KeyboardAggregator { pub fn new(tx: Sender) -> Self { Self { tx, devices: Vec::new(), pressed_keys: HashSet::new(), } } /// Discover all /dev/input/event* devices, filter ones with EV_KEY, and grab them. pub fn init_devices(&mut self) -> Result<()> { let paths = std::fs::read_dir("/dev/input") .context("Failed to read /dev/input")?; for entry in paths { let entry = entry?; let path = entry.path(); // skip anything that isn't "event*" if !path.file_name().unwrap_or_default().to_string_lossy().starts_with("event") { continue; } let mut dev = Device::open(&path).with_context(|| format!("opening {:?}", path))?; let maybe_keys = dev.supported_keys(); if let Some(supported) = maybe_keys { if supported.iter().next().is_none() { // no real keys continue; } } else { // Not a keyboard at all continue; } // Attempt to grab dev.grab().with_context(|| format!("grabbing {:?}", path))?; tracing::info!("Grabbed keyboard device: {:?}", path); self.devices.push(dev); } if self.devices.is_empty() { bail!("No keyboard devices found or none grabbed successfully."); } Ok(()) } /// Main loop: read events from all devices in a round-robin style /// building HID reports as needed. If MAGIC_CHORD is detected, exit. pub async fn run(&mut self) -> Result<()> { let mut tick = interval(Duration::from_millis(10)); loop { // We'll poll each device in turn for i in 0..self.devices.len() { // Non-blocking fetch let evs = match self.devices[i].fetch_events() { Ok(iter) => iter.collect::>(), Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Vec::new(), Err(e) => { tracing::error!("Device read error: {e}"); Vec::new() } }; for ev in evs { self.handle_event(ev)?; } } if self.is_magic_chord() { tracing::warn!("Magic chord pressed; stopping navka-client."); // ungrab all before exit for dev in &mut self.devices { let _ = dev.ungrab(); } std::process::exit(0); } tick.tick().await; } } fn handle_event(&mut self, ev: InputEvent) -> Result<()> { // We only care about KEY events if ev.event_type() == EventType::KEY { let code = KeyCode::new(ev.code()); let value = ev.value(); match value { 1 => { self.pressed_keys.insert(code); } 0 => { self.pressed_keys.remove(&code); } 2 => { /* repeats, if needed */ } _ => {} } // Build new HID report let report_bytes = self.build_hid_report(); let _ = self.tx.try_send(HidReport { data: report_bytes.to_vec(), }); } Ok(()) } fn build_hid_report(&self) -> [u8; 8] { // Byte 0: modifiers (bitmask) // Byte 1: reserved // Byte 2..7: up to 6 keys // We'll do a naive approach: gather up to 6 non-modifier codes. let mut report = [0u8; 8]; let mut normal_keys = Vec::new(); let mut modifier_mask = 0u8; for &key in &self.pressed_keys { if let Some(modbits) = is_modifier(key) { // e.g. KEY_LEFTSHIFT => 0x02 modifier_mask |= modbits; } else if let Some(usage) = keycode_to_usage(key) { // Normal key normal_keys.push(usage); } } report[0] = modifier_mask; // Byte[1] is reserved 0 for (i, code) in normal_keys.into_iter().take(6).enumerate() { report[2 + i] = code; } report } fn is_magic_chord(&self) -> bool { // All the keys in MAGIC_CHORD must be pressed MAGIC_CHORD.iter().all(|k| self.pressed_keys.contains(k)) } }