//! **La entrada, leída del kernel.** Sin libinput, sin X, sin Wayland. //! //! Un instalador que arranca de un pendrive no tiene servidor de nada: lo único //! que hay es `/dev/input/event*`, que el kernel publica para cualquier teclado //! o puntero conectado. Este módulo los abre, decodifica los eventos y los //! traduce al modelo de `llimphi-entrada` — que existe justamente para esto: //! *«procesar UNA TECLA arrastraba el windowing de escritorio entero»*. //! //! # Teclado **y** puntero, en un solo lector //! //! Se llamaba `teclado` y leía sólo teclas. El puntero **no puede ser un segundo //! módulo que abra los mismos nodos**: un `/dev/input/eventN` es una cola, y el //! que lee primero se lleva el evento — dos lectores del mismo aparato se roban //! las pulsaciones entre sí. Así que hay un solo lector y una sola cola de //! salida ([`Evento`]). //! //! Del puntero se lee lo **relativo** (`EV_REL`) y los botones, que es lo que //! emiten tanto un mouse USB como el nodo «Mouse» de un touchpad. Medido en la //! máquina del operador el 2026-10-09: su touchpad publica `event8` //! («DELL0A7E:00 04F3:3147 Mouse», `REL=3`) además del nodo multitáctil, y su //! mouse USB publica `event20` (`REL=903`). Los dos entran por este camino. //! **Lo que NO se lee es `EV_ABS`**: un panel táctil o una tableta necesitan el //! rango de cada eje, que se pide por `ioctl` (`EVIOCGABS`) y no vive en ningún //! archivo — y sin el rango no hay forma de escalar a píxeles sin inventar. //! //! # Lo que este módulo decide, y por qué //! //! - **Abre todos los `event*`**, no «el primero». En una máquina hay varios —el //! teclado, el touchpad, los botones de encendido— y cuál es cuál cambia entre //! arranques. Abrir todos y filtrar por lo que emiten es lo único estable. //! - **No bloquea.** Un instalador tiene que poder repintar aunque nadie toque //! una tecla; un `read` bloqueante dejaría la pantalla congelada hasta la //! próxima pulsación. //! - **Ignora las repeticiones automáticas** (`value == 2`). Una tecla sostenida //! en un menú de cuatro opciones lo recorre entero antes de que alguien //! levante el dedo. use std::io::Read; use std::os::fd::AsRawFd; use llimphi_entrada::{Key, KeyEvent, KeyState, Modifiers, NamedKey}; /// Un `struct input_event` de Linux en 64 bits: `timeval` (16) + tipo (2) + /// código (2) + valor (4). El tamaño es parte del ABI del kernel y no cambia. const TAM_EVENTO: usize = 24; /// `EV_KEY`: el tipo de evento que trae pulsaciones — de teclas y de botones. const EV_KEY: u16 = 1; /// `EV_REL`: movimiento relativo. Es lo que emite un mouse y el nodo «Mouse» de /// un touchpad. const EV_REL: u16 = 2; const REL_X: u16 = 0; const REL_Y: u16 = 1; /// Los tres botones, con los códigos de `input-event-codes.h` (`BTN_LEFT` = /// 0x110). Están en el mismo espacio que las teclas, y por eso `tecla()` los /// descarta: pasarlos por el mapa del teclado daría una letra cualquiera. const BTN_LEFT: u16 = 0x110; const BTN_RIGHT: u16 = 0x111; const BTN_MIDDLE: u16 = 0x112; /// Un botón del puntero. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Boton { Izquierdo, Derecho, Medio, } /// Lo que llegó de los aparatos de entrada, en el orden en que llegó. /// /// Una sola cola para teclado y puntero, porque es **una sola cola en el /// kernel**: un mouse emite sus botones como `EV_KEY` en el mismo nodo que su /// movimiento, y separarlos en dos lectores haría que uno le robara los eventos /// al otro. #[derive(Debug, Clone)] pub enum Evento { Tecla(KeyEvent), /// El puntero quedó en esa posición, **en píxeles de la pantalla**. Se emite /// **uno por lectura**, no uno por evento del kernel: un mouse manda decenas /// de `EV_REL` por gesto y repintar en cada uno no mueve el cursor más /// suave, sólo gasta cuadros. Puntero { x: f32, y: f32 }, /// Un botón, con la posición donde estaba el puntero en ese momento. Boton { boton: Boton, apretado: bool, x: f32, y: f32 }, } /// Todos los aparatos de entrada de la máquina, abiertos y listos para leer. pub struct Entradas { dispositivos: Vec, /// Cuáles ya están abiertos, por nombre de nodo (`event3`). Sin esto, /// refrescar los abriría de nuevo en cada pasada. abiertos: alloc_set::Set, mods: Modifiers, /// Dónde está el puntero, en píxeles. Arranca en el centro: un cursor en /// `(0,0)` se lee como que no hay cursor. x: f32, y: f32, /// El tamaño de la pantalla, para no dejar salir el cursor. Un cursor que se /// va del borde no vuelve: el movimiento es relativo y no hay nada que lo /// traiga de nuevo. ancho: f32, alto: f32, /// ¿Alguno de los aparatos abiertos emite movimiento relativo? /// /// Sale de `/sys/class/input//device/capabilities/rel`, que es un /// **archivo** —no un `ioctl`— y trae la máscara de ejes. Hace falta saberlo /// *antes* de que alguien mueva el mouse: la pantalla tiene que poder decir /// con qué se maneja, y decir «tocá» donde no hay puntero es mandar a hacer /// algo imposible. hay_puntero: bool, } /// Un conjunto de cadenas sin traer nada: son diez nombres cortos y el crate no /// tiene dependencias fuera de las de la casa. mod alloc_set { #[derive(Default)] pub struct Set(Vec); impl Set { pub fn contiene(&self, s: &str) -> bool { self.0.iter().any(|x| x == s) } pub fn meter(&mut self, s: String) { self.0.push(s); } pub fn cuantos(&self) -> usize { self.0.len() } pub fn nombres(&self) -> &[String] { &self.0 } } } impl Entradas { /// Abre lo que haya. **No falla si no hay ninguno**: una máquina sin teclado /// es un caso real —un servidor, una prueba automática— y lo que corresponde /// es seguir pintando, no negarse a arrancar. /// /// El tamaño de la pantalla es para acotar el cursor; con `(0, 0)` el /// puntero no se mueve de su sitio, que es lo correcto cuando quien llama /// sólo quiere enumerar los aparatos. pub fn abrir(ancho: u16, alto: u16) -> Self { let mut t = Entradas { dispositivos: Vec::new(), abiertos: alloc_set::Set::default(), mods: Modifiers::default(), x: ancho as f32 / 2.0, y: alto as f32 / 2.0, ancho: ancho as f32, alto: alto as f32, hay_puntero: false, }; t.refrescar(); t } /// ¿Hay con qué apuntar? Ver [`Entradas::hay_puntero`]. pub fn tiene_puntero(&self) -> bool { self.hay_puntero } /// Dónde está el puntero ahora, en píxeles. pub fn puntero(&self) -> (f32, f32) { (self.x, self.y) } /// Abre lo que haya aparecido desde la última vez. Devuelve cuántos nuevos. /// /// **Existe porque los dispositivos no están todos al arrancar.** Un teclado /// USB enumera de forma asíncrona y tarda segundos; el del portátil puede /// registrarse después que el video. Con un solo escaneo al inicio, la /// pantalla queda pintada y sorda para siempre — y desde afuera se ve igual /// que una máquina colgada. Medido en metal el 2026-09-29: el instalador /// dibujó los discos y no respondió a ninguna tecla, mientras la consola de /// texto de la otra pantalla sí las recibía. pub fn refrescar(&mut self) -> usize { self.refrescar_en("/dev/input") } /// Como [`Self::refrescar`] pero sobre otro directorio. Es lo que permite /// probarlo sin `/dev`: un directorio con archivos `eventN` se comporta /// igual para lo que acá importa, que es **cuáles se abren y cuáles no**. pub fn refrescar_en(&mut self, raiz: &str) -> usize { let mut nuevos = 0; let dispositivos = &mut self.dispositivos; let abiertos = &mut self.abiertos; let hay_puntero = &mut self.hay_puntero; if let Ok(dir) = std::fs::read_dir(raiz) { for e in dir.filter_map(|e| e.ok()) { let nombre = e.file_name(); let Some(n) = nombre.to_str() else { continue }; if !n.starts_with("event") || abiertos.contiene(n) { continue; } let Ok(f) = std::fs::File::open(e.path()) else { continue }; // Sin bloquear: la pantalla tiene que poder repintarse aunque // nadie toque nada. unsafe { let fd = f.as_raw_fd(); let flags = libc_fcntl_getfl(fd); if flags >= 0 { libc_fcntl_setfl(fd, flags | O_NONBLOCK); } } dispositivos.push(f); // ¿Este aparato apunta? Se pregunta al abrirlo y no cuando // llega el primer movimiento: la pantalla tiene que poder decir // con qué se maneja **antes** de que nadie toque nada. if apunta(n) { *hay_puntero = true; } abiertos.meter(n.to_string()); nuevos += 1; } } nuevos } /// Cuántos dispositivos de entrada hay abiertos, y cómo se llaman. /// /// **El conteo importa y el nombre más.** `vacio()` sólo dice que hay /// *alguno*, y en cualquier portátil siempre hay alguno —el botón de /// encendido, la tapa— así que «hay teclado» era verdad y no servía: el /// teclado podía no estar. El nombre sale de /// `/sys/class/input//device/name`, que es un archivo, no un ioctl. pub fn descripcion(&self) -> Vec { self.abiertos .nombres() .iter() .map(|n| { let ruta = format!("/sys/class/input/{n}/device/name"); match std::fs::read_to_string(&ruta) { Ok(s) => format!("{n} ({})", s.trim()), Err(_) => n.clone(), } }) .collect() } /// Cuántos hay abiertos. pub fn cuantos(&self) -> usize { self.abiertos.cuantos() } /// `true` si no se pudo abrir ninguno. Quien llame decide qué hacer — y /// **decirlo** es mejor que una pantalla que no responde sin explicación. pub fn vacio(&self) -> bool { self.dispositivos.is_empty() } /// Lo que llegó desde la última vez. Lista vacía = nadie tocó nada, que es /// el caso normal. /// /// **El movimiento se junta en uno solo.** Un gesto de mouse son decenas de /// `EV_REL`; emitir uno por cada uno haría un cuadro por cada uno, y el /// cursor no se mueve más suave por eso — se mueve igual y con la pantalla /// ocupada. Los botones sí salen uno por uno, y cada uno lleva la posición /// que el puntero tenía **en ese momento**: si el gesto fue mover-y-clickear /// dentro de la misma lectura, el click no puede resolverse contra la /// posición anterior. pub fn leer(&mut self) -> Vec { let mut fuera = Vec::new(); let (x0, y0) = (self.x, self.y); let mut buf = [0u8; TAM_EVENTO * 16]; for d in &mut self.dispositivos { let Ok(n) = d.read(&mut buf) else { continue }; for trozo in buf[..n].chunks_exact(TAM_EVENTO) { let tipo = u16::from_ne_bytes([trozo[16], trozo[17]]); let codigo = u16::from_ne_bytes([trozo[18], trozo[19]]); let valor = i32::from_ne_bytes([trozo[20], trozo[21], trozo[22], trozo[23]]); if tipo == EV_REL { // Acotado a la pantalla: el movimiento es relativo, así que // un cursor que se va del borde no vuelve nunca. match codigo { REL_X => self.x = (self.x + valor as f32).clamp(0.0, self.ancho - 1.0), REL_Y => self.y = (self.y + valor as f32).clamp(0.0, self.alto - 1.0), // La rueda queda sin usar a propósito: hoy ninguna // pantalla de esta cara tiene nada que rodar, y un // evento que nadie consume es una promesa sin cumplir. _ => {} } continue; } if tipo != EV_KEY { continue; } // 0 suelta, 1 pulsa, 2 repetición automática. La repetición se // ignora: una tecla sostenida recorrería un menú entero. let estado = match valor { 0 => KeyState::Released, 1 => KeyState::Pressed, _ => continue, }; if let Some(b) = boton(codigo) { fuera.push(Evento::Boton { boton: b, apretado: estado == KeyState::Pressed, x: self.x, y: self.y, }); continue; } if let Some(m) = modificador(codigo) { aplicar_modificador(&mut self.mods, m, estado == KeyState::Pressed); continue; } let Some(key) = tecla(codigo, self.mods.shift) else { continue }; // `text` es lo que un campo inserta cuando la tecla produce // texto. Dejarlo en `None` no rompe la navegación —las flechas // no producen nada— pero deja mudo cualquier campo donde alguien // tenga que escribir, que acá es el que pide el nombre del // disco antes de formatearlo. let text = match &key { Key::Character(s) => Some(s.clone()), Key::Named(_) => None, }; fuera.push(Evento::Tecla(KeyEvent { key, state: estado, text, modifiers: self.mods, repeat: false, })); } } // El movimiento, una vez y al final. if self.x != x0 || self.y != y0 { fuera.push(Evento::Puntero { x: self.x, y: self.y }); } fuera } } /// ¿Este nodo de `/dev/input` emite movimiento relativo en los dos ejes? /// /// Se lee de `/sys/class/input//device/capabilities/rel`, que trae la /// máscara de ejes en hexadecimal. Es un **archivo**: el `ioctl` equivalente /// (`EVIOCGBIT`) obligaría a traerse `libc` y a escribir `unsafe` por un dato /// que el kernel ya publica como texto. /// /// Se exige `REL_X` **y** `REL_Y` (bits 0 y 1, máscara `3`): hay aparatos que /// declaran sólo la rueda —el `SEM USB Keyboard Consumer Control` de la máquina /// del operador declara `1040`— y ésos no son punteros. fn apunta(nodo: &str) -> bool { apunta_en("/sys/class/input", nodo) } fn apunta_en(raiz: &str, nodo: &str) -> bool { let ruta = format!("{raiz}/{nodo}/device/capabilities/rel"); let Ok(txt) = std::fs::read_to_string(&ruta) else { return false }; // La máscara puede venir en varias palabras separadas por espacios, de la // más alta a la más baja; los ejes 0 y 1 están en la última. let Some(ultima) = txt.split_whitespace().last() else { return false }; let Ok(bits) = u64::from_str_radix(ultima, 16) else { return false }; bits & 0b11 == 0b11 } /// Del código del kernel al botón. Los tres primeros y nada más: un mouse de /// juego manda ocho y los que no tienen significado acá no se inventan. fn boton(codigo: u16) -> Option { match codigo { BTN_LEFT => Some(Boton::Izquierdo), BTN_RIGHT => Some(Boton::Derecho), BTN_MIDDLE => Some(Boton::Medio), _ => None, } } enum Mod { Shift, Ctrl, Alt, } fn modificador(codigo: u16) -> Option { match codigo { 42 | 54 => Some(Mod::Shift), 29 | 97 => Some(Mod::Ctrl), 56 | 100 => Some(Mod::Alt), _ => None, } } fn aplicar_modificador(m: &mut Modifiers, cual: Mod, apretado: bool) { match cual { Mod::Shift => m.shift = apretado, Mod::Ctrl => m.ctrl = apretado, Mod::Alt => m.alt = apretado, } } /// Del código del kernel a la tecla del modelo. /// /// Los números son los de `input-event-codes.h` y **no cambian**: son ABI del /// kernel desde hace veinte años. Por eso van acá y no en un archivo de /// configuración que alguien tendría que mantener. /// /// Es un teclado **US sin mapa de teclado**: alcanza para navegar, para escribir /// el nombre de un dispositivo y para confirmar. Un `xkb` completo es otro /// trabajo, y meterlo acá arrastraría lo que este camino existe para no /// arrastrar. fn tecla(codigo: u16, shift: bool) -> Option { let nombrada = |n: NamedKey| Some(Key::Named(n)); match codigo { 1 => nombrada(NamedKey::Escape), 14 => nombrada(NamedKey::Backspace), 15 => nombrada(NamedKey::Tab), 28 | 96 => nombrada(NamedKey::Enter), 103 => nombrada(NamedKey::ArrowUp), 108 => nombrada(NamedKey::ArrowDown), 105 => nombrada(NamedKey::ArrowLeft), 106 => nombrada(NamedKey::ArrowRight), 102 => nombrada(NamedKey::Home), 107 => nombrada(NamedKey::End), 111 => nombrada(NamedKey::Delete), 57 => Some(Key::Character(" ".into())), _ => caracter(codigo, shift).map(|c| Key::Character(c.to_string())), } } fn caracter(codigo: u16, shift: bool) -> Option { const FILA1: &str = "1234567890-="; const FILA1S: &str = "!@#$%^&*()_+"; const FILA2: &str = "qwertyuiop[]"; const FILA3: &str = "asdfghjkl;'"; const FILA4: &str = "zxcvbnm,./"; let de = |s: &str, i: usize| s.chars().nth(i); let c = match codigo { 2..=13 => de(if shift { FILA1S } else { FILA1 }, codigo as usize - 2), 16..=27 => de(FILA2, codigo as usize - 16), 30..=40 => de(FILA3, codigo as usize - 30), 44..=53 => de(FILA4, codigo as usize - 44), _ => None, }?; Some(if shift { c.to_ascii_uppercase() } else { c }) } // Las dos llamadas a `fcntl` que hacen falta, sin traerse el crate `libc` // entero por dos constantes: este crate va adentro de un initramfs y cada // dependencia se paga en bytes. const O_NONBLOCK: i32 = 0o4000; const F_GETFL: i32 = 3; const F_SETFL: i32 = 4; unsafe extern "C" { fn fcntl(fd: i32, cmd: i32, ...) -> i32; } unsafe fn libc_fcntl_getfl(fd: i32) -> i32 { unsafe { fcntl(fd, F_GETFL) } } unsafe fn libc_fcntl_setfl(fd: i32, flags: i32) -> i32 { unsafe { fcntl(fd, F_SETFL, flags) } } #[cfg(test)] mod tests { use super::*; #[test] fn las_flechas_y_el_enter_son_lo_que_dicen() { // Son los códigos de `input-event-codes.h`, que son ABI del kernel. assert_eq!(tecla(103, false), Some(Key::Named(NamedKey::ArrowUp))); assert_eq!(tecla(108, false), Some(Key::Named(NamedKey::ArrowDown))); assert_eq!(tecla(28, false), Some(Key::Named(NamedKey::Enter))); assert_eq!(tecla(1, false), Some(Key::Named(NamedKey::Escape))); } #[test] fn se_puede_escribir_lo_que_pide_la_confirmacion_fuerte() { // `dev sda` con los códigos del kernel: d-e-v-s-d-a. Si alguna no // saliera, el campo donde hay que escribir el nombre del disco sería // imposible de completar — y esa confirmación existe justamente para // los discos de los que no se sabe qué tienen. let escrito: String = [32u16, 18, 47, 31, 32, 30] .iter() .filter_map(|c| match tecla(*c, false) { Some(Key::Character(s)) => s.chars().next(), _ => None, }) .collect(); assert_eq!(escrito, "devsda"); } #[test] fn el_shift_cambia_la_fila_de_numeros_y_no_solo_las_letras() { assert_eq!(caracter(2, false), Some('1')); assert_eq!(caracter(2, true), Some('!')); assert_eq!(caracter(16, false), Some('q')); assert_eq!(caracter(16, true), Some('Q')); } #[test] fn una_tecla_que_no_conozco_no_inventa_un_caracter() { // Mejor perder una tecla rara que meter basura en un campo donde alguien // está escribiendo el nombre del disco que va a formatear. assert_eq!(tecla(0, false), None); assert_eq!(tecla(200, false), None); } } #[cfg(test)] mod tests_escaneo { use super::*; fn tmp(nombre: &str) -> std::path::PathBuf { let d = std::env::temp_dir().join(format!("llimphi-drm-{}-{nombre}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); d } /// **El caso que costó un viaje al metal.** Los dispositivos no están todos /// al arrancar: un teclado USB enumera de forma asíncrona. Con un solo /// escaneo al inicio, la pantalla queda pintada y sorda para siempre. #[test] fn toma_los_que_aparecen_despues() { let d = tmp("tarde"); std::fs::write(d.join("event0"), b"").unwrap(); let mut t = Entradas { dispositivos: Vec::new(), abiertos: alloc_set::Set::default(), mods: Modifiers::default(), x: 0.0, y: 0.0, ancho: 100.0, alto: 100.0, hay_puntero: false, }; assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1); // Aparece el teclado, tarde. std::fs::write(d.join("event1"), b"").unwrap(); assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1, "el nuevo se toma"); assert_eq!(t.cuantos(), 2); // Y no se reabre lo que ya estaba: si no, cada pasada duplicaría los // descriptores hasta quedarse sin ninguno. assert_eq!(t.refrescar_en(d.to_str().unwrap()), 0); assert_eq!(t.cuantos(), 2); let _ = std::fs::remove_dir_all(&d); } /// Sólo los nodos de evento. En `/dev/input` también viven `mice`, `mouse0` /// y el directorio `by-path`, y abrirlos como si fueran teclados da basura. #[test] fn solo_los_nodos_de_evento() { let d = tmp("filtro"); for n in ["event0", "mice", "mouse0", "js0"] { std::fs::write(d.join(n), b"").unwrap(); } let mut t = Entradas { dispositivos: Vec::new(), abiertos: alloc_set::Set::default(), mods: Modifiers::default(), x: 0.0, y: 0.0, ancho: 100.0, alto: 100.0, hay_puntero: false, }; assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1); // Se mira el NODO y no el texto completo: `descripcion()` le pide el // nombre al sysfs de la máquina donde corre, y ahí `event0` es lo que // sea que tenga esa máquina («Lid Switch» en la primera donde se probó). // Lo que este test fija es el filtro, no el nombre. let d0 = t.descripcion(); assert_eq!(d0.len(), 1); assert!(d0[0].starts_with("event0"), "{:?}", d0); let _ = std::fs::remove_dir_all(&d); } } #[cfg(test)] mod pruebas_del_puntero { use super::*; use std::io::Write; /// Arma un `struct input_event` como el que escribe el kernel. fn ev(tipo: u16, codigo: u16, valor: i32) -> [u8; TAM_EVENTO] { let mut b = [0u8; TAM_EVENTO]; b[16..18].copy_from_slice(&tipo.to_ne_bytes()); b[18..20].copy_from_slice(&codigo.to_ne_bytes()); b[20..24].copy_from_slice(&valor.to_ne_bytes()); b } /// Un lector sobre un archivo con eventos ya escritos. Es el mismo camino /// que `/dev/input`: `read` de un fd y decodificar — lo único que cambia es /// quién escribió los bytes. fn con_eventos(nombre: &str, eventos: &[[u8; TAM_EVENTO]]) -> Entradas { let d = std::env::temp_dir() .join(format!("llimphi-drm-pt-{}-{nombre}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); let mut f = std::fs::File::create(d.join("event0")).unwrap(); for e in eventos { f.write_all(e).unwrap(); } drop(f); let mut t = Entradas { dispositivos: Vec::new(), abiertos: alloc_set::Set::default(), mods: Modifiers::default(), x: 50.0, y: 50.0, ancho: 100.0, alto: 100.0, hay_puntero: false, }; t.refrescar_en(d.to_str().unwrap()); let _ = std::fs::remove_dir_all(&d); t } /// 🔴 **El movimiento se junta en UNO.** Un gesto de mouse son decenas de /// `EV_REL`; un cuadro por cada uno no mueve el cursor más suave, sólo deja /// la pantalla ocupada. #[test] fn muchos_movimientos_dan_un_solo_evento() { let mut t = con_eventos( "junta", &[ ev(EV_REL, REL_X, 3), ev(EV_REL, REL_Y, 4), ev(EV_REL, REL_X, 3), ev(EV_REL, REL_Y, 4), ], ); let salida = t.leer(); assert_eq!(salida.len(), 1, "un gesto, un evento: {salida:?}"); match salida[0] { Evento::Puntero { x, y } => assert_eq!((x, y), (56.0, 58.0)), ref otro => panic!("esperaba un movimiento: {otro:?}"), } } /// 🔴 **El click lleva la posición de ESE momento.** Si el gesto fue /// mover-y-clickear dentro de la misma lectura, resolver el click contra la /// posición anterior lo pondría en otro lado — y en esta pantalla «otro /// lado» es otra partición. #[test] fn el_boton_usa_la_posicion_del_momento() { let mut t = con_eventos( "clic", &[ev(EV_REL, REL_X, 10), ev(EV_KEY, BTN_LEFT, 1), ev(EV_REL, REL_X, 10)], ); let salida = t.leer(); let apretado = salida .iter() .find_map(|e| match e { Evento::Boton { boton: Boton::Izquierdo, apretado: true, x, y } => Some((*x, *y)), _ => None, }) .expect("tiene que haber un botón apretado"); assert_eq!(apretado, (60.0, 50.0), "la posición del click es la de ese instante"); // Y el movimiento final, una sola vez y con el total. let movio = salida .iter() .filter_map(|e| match e { Evento::Puntero { x, y } => Some((*x, *y)), _ => None, }) .collect::>(); assert_eq!(movio, vec![(70.0, 50.0)]); } /// El cursor **no se va de la pantalla**: el movimiento es relativo y lo que /// sale por el borde no vuelve nunca. #[test] fn el_cursor_no_se_escapa_por_el_borde() { let mut t = con_eventos("borde", &[ev(EV_REL, REL_X, -500), ev(EV_REL, REL_Y, 500)]); t.leer(); assert_eq!(t.puntero(), (0.0, 99.0), "acotado a la pantalla de 100x100"); } /// Los botones **no pasan por el mapa del teclado**. Están en el mismo /// espacio de códigos que las teclas, así que sin filtrarlos un click /// escribiría una letra en el campo donde se tipea el nombre del disco. #[test] fn un_click_no_escribe_una_letra() { let mut t = con_eventos("noletra", &[ev(EV_KEY, BTN_LEFT, 1)]); let salida = t.leer(); assert!( !salida.iter().any(|e| matches!(e, Evento::Tecla(_))), "un botón no es una tecla: {salida:?}" ); assert_eq!(tecla(BTN_LEFT, false), None, "y el mapa del teclado no lo conoce"); } /// Y una tecla sigue siendo una tecla: el brazo de control de lo de arriba. #[test] fn una_tecla_sigue_llegando_como_tecla() { let mut t = con_eventos("tecla", &[ev(EV_KEY, 28, 1)]); let salida = t.leer(); assert!( matches!(&salida[..], [Evento::Tecla(k)] if k.key == Key::Named(NamedKey::Enter)), "{salida:?}" ); } /// 🔴 **Declarar sólo la rueda NO es ser un puntero.** El /// `SEM USB Keyboard Consumer Control` de la máquina del operador declara /// `REL=1040` —rueda y nada más— y tomarlo por mouse haría que la pantalla /// prometa un cursor que no existe. #[test] fn la_rueda_sola_no_cuenta_como_puntero() { let d = std::env::temp_dir().join(format!("llimphi-drm-cap-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); for (nodo, mascara) in [("event0", "3"), ("event1", "1040"), ("event2", "0")] { let dir = d.join(nodo).join("device").join("capabilities"); std::fs::create_dir_all(&dir).unwrap(); std::fs::write(dir.join("rel"), mascara).unwrap(); } let raiz = d.to_str().unwrap(); assert!(apunta_en(raiz, "event0"), "REL_X|REL_Y sí es un puntero"); assert!(!apunta_en(raiz, "event1"), "sólo la rueda no"); assert!(!apunta_en(raiz, "event2"), "y un teclado tampoco"); assert!(!apunta_en(raiz, "event9"), "ni un nodo que no existe"); let _ = std::fs::remove_dir_all(&d); } }