lesavka/client/src/input/keyboard.rs

161 lines
5.3 KiB
Rust

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<HidReport>,
// The list of evdev devices we are reading
devices: Vec<Device>,
// Pressed keys for building HID reports
pressed_keys: HashSet<KeyCode>,
}
impl KeyboardAggregator {
pub fn new(tx: Sender<HidReport>) -> 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::<Vec<_>>(),
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))
}
}