// client/src/input/inputs.rs use anyhow::{bail, Context, Result}; use evdev::{Device, EventType, KeyCode, RelativeAxisCode}; use tokio::{sync::broadcast::Sender, time::{interval, Duration}}; use tracing::{debug, info, warn}; use lesavka_common::lesavka::{KeyboardReport, MouseReport}; use super::{keyboard::KeyboardAggregator, mouse::MouseAggregator}; use crate::layout::{Layout, apply as apply_layout}; pub struct InputAggregator { kbd_tx: Sender, mou_tx: Sender, dev_mode: bool, released: bool, magic_active: bool, keyboards: Vec, mice: Vec, } impl InputAggregator { pub fn new(dev_mode: bool, kbd_tx: Sender, mou_tx: Sender) -> Self { Self { kbd_tx, mou_tx, dev_mode, released: false, magic_active: false, keyboards: Vec::new(), mice: Vec::new() } } /// Called once at startup: enumerates input devices, /// classifies them, and constructs a aggregator struct per type. pub fn init(&mut self) -> Result<()> { let paths = std::fs::read_dir("/dev/input") .context("Failed to read /dev/input")?; let mut found_any = false; for entry in paths { let entry = entry?; let path = entry.path(); // skip anything that isn't "event*" if !path.file_name().map_or(false, |f| f.to_string_lossy().starts_with("event")) { continue; } // ─── open the event node read-write *without* unsafe ────────── let mut dev = match Device::open(&path) { Ok(d) => d, Err(e) => { warn!("❌ open {}: {e}", path.display()); continue; } }; // non-blocking so fetch_events never stalls the whole loop dev.set_nonblocking(true).with_context(|| format!("set_non_blocking {:?}", path))?; match classify_device(&dev) { DeviceKind::Keyboard => { dev.grab().with_context(|| format!("grabbing keyboard {path:?}"))?; info!("🤏🖱️ Grabbed keyboard {:?}", dev.name().unwrap_or("UNKNOWN")); // pass dev_mode to aggregator // let kbd_agg = KeyboardAggregator::new(dev, self.dev_mode); let kbd_agg = KeyboardAggregator::new(dev, self.dev_mode, self.kbd_tx.clone()); self.keyboards.push(kbd_agg); found_any = true; continue; } DeviceKind::Mouse => { dev.grab().with_context(|| format!("grabbing mouse {path:?}"))?; info!("🤏⌨️ Grabbed mouse {:?}", dev.name().unwrap_or("UNKNOWN")); // let mouse_agg = MouseAggregator::new(dev); let mouse_agg = MouseAggregator::new(dev, self.dev_mode, self.mou_tx.clone()); self.mice.push(mouse_agg); found_any = true; continue; } DeviceKind::Other => { debug!("Skipping non-kbd/mouse device: {:?}", dev.name().unwrap_or("UNKNOWN")); continue; } } } if !found_any { bail!("No suitable keyboard/mouse devices found or none grabbed."); } Ok(()) } /// We spawn the sub-aggregators in a loop or using separate tasks. /// (For a real system: you'd spawn a separate task for each aggregator.) pub async fn run(&mut self) -> Result<()> { // Example approach: poll each aggregator in a simple loop let mut tick = interval(Duration::from_millis(10)); let mut current = Layout::SideBySide; loop { let magic_now = self.keyboards.iter().any(|k| k.magic_grab()); let magic_left = self.keyboards.iter().any(|k| k.magic_left()); let magic_right = self.keyboards.iter().any(|k| k.magic_right()); let mut want_kill = false; for kbd in &mut self.keyboards { kbd.process_events(); want_kill |= kbd.magic_kill(); } if magic_now && !self.magic_active { self.toggle_grab(); } if (magic_left || magic_right) && self.magic_active { current = match current { Layout::SideBySide => Layout::FullLeft, Layout::FullLeft => Layout::FullRight, Layout::FullRight => Layout::SideBySide, }; apply_layout(current); } if want_kill { warn!("🧙 magic chord - killing 🪄 AVADA KEDAVRA!!! 💥💀⚰️"); std::process::exit(0); } for mouse in &mut self.mice { mouse.process_events(); } self.magic_active = magic_now; tick.tick().await; } } fn toggle_grab(&mut self) { if self.released { tracing::info!("🧙 magic chord - restricting devices 🪄 IMPERIUS!!! 🎮🔒"); } else { tracing::info!("🧙 magic chord - freeing devices 🪄 EXPELLIARMUS!!! 🔓🕊️"); } for k in &mut self.keyboards { k.set_grab(self.released); k.set_send(self.released); } for m in &mut self.mice { m.set_grab(self.released); m.set_send(self.released); } self.released = !self.released; } } /// The classification function fn classify_device(dev: &Device) -> DeviceKind { let evbits = dev.supported_events(); // Keyboard logic if evbits.contains(EventType::KEY) { if let Some(keys) = dev.supported_keys() { if keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_ENTER) { return DeviceKind::Keyboard; } } } // Mouse logic if evbits.contains(EventType::RELATIVE) { if let (Some(rel), Some(keys)) = (dev.supported_relative_axes(), dev.supported_keys()) { let has_xy = rel.contains(RelativeAxisCode::REL_X) && rel.contains(RelativeAxisCode::REL_Y); let has_btn = keys.contains(KeyCode::BTN_LEFT) || keys.contains(KeyCode::BTN_RIGHT); if has_xy && has_btn { return DeviceKind::Mouse; } } } DeviceKind::Other } /// Internal enum for device classification #[derive(Debug, Clone, Copy)] enum DeviceKind { Keyboard, Mouse, Other, }