tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1//! **La entrada, leída del kernel.** Sin libinput, sin X, sin Wayland.
2//!
3//! Un instalador que arranca de un pendrive no tiene servidor de nada: lo único
4//! que hay es `/dev/input/event*`, que el kernel publica para cualquier teclado
5//! o puntero conectado. Este módulo los abre, decodifica los eventos y los
6//! traduce al modelo de `llimphi-entrada` — que existe justamente para esto:
7//! *«procesar UNA TECLA arrastraba el windowing de escritorio entero»*.
8//!
9//! # Teclado **y** puntero, en un solo lector
10//!
11//! Se llamaba `teclado` y leía sólo teclas. El puntero **no puede ser un segundo
12//! módulo que abra los mismos nodos**: un `/dev/input/eventN` es una cola, y el
13//! que lee primero se lleva el evento — dos lectores del mismo aparato se roban
14//! las pulsaciones entre sí. Así que hay un solo lector y una sola cola de
15//! salida ([`Evento`]).
16//!
17//! Del puntero se lee lo **relativo** (`EV_REL`) y los botones, que es lo que
18//! emiten tanto un mouse USB como el nodo «Mouse» de un touchpad. Medido en la
19//! máquina del operador el 2026-10-09: su touchpad publica `event8`
20//! («DELL0A7E:00 04F3:3147 Mouse», `REL=3`) además del nodo multitáctil, y su
21//! mouse USB publica `event20` (`REL=903`). Los dos entran por este camino.
22//! **Lo que NO se lee es `EV_ABS`**: un panel táctil o una tableta necesitan el
23//! rango de cada eje, que se pide por `ioctl` (`EVIOCGABS`) y no vive en ningún
24//! archivo — y sin el rango no hay forma de escalar a píxeles sin inventar.
25//!
26//! # Lo que este módulo decide, y por qué
27//!
28//! - **Abre todos los `event*`**, no «el primero». En una máquina hay varios —el
29//! teclado, el touchpad, los botones de encendido— y cuál es cuál cambia entre
30//! arranques. Abrir todos y filtrar por lo que emiten es lo único estable.
31//! - **No bloquea.** Un instalador tiene que poder repintar aunque nadie toque
32//! una tecla; un `read` bloqueante dejaría la pantalla congelada hasta la
33//! próxima pulsación.
34//! - **Ignora las repeticiones automáticas** (`value == 2`). Una tecla sostenida
35//! en un menú de cuatro opciones lo recorre entero antes de que alguien
36//! levante el dedo.
37
38use std::io::Read;
39use std::os::fd::AsRawFd;
40
41use llimphi_entrada::{Key, KeyEvent, KeyState, Modifiers, NamedKey};
42
43/// Un `struct input_event` de Linux en 64 bits: `timeval` (16) + tipo (2) +
44/// código (2) + valor (4). El tamaño es parte del ABI del kernel y no cambia.
45const TAM_EVENTO: usize = 24;
46/// `EV_KEY`: el tipo de evento que trae pulsaciones — de teclas y de botones.
47const EV_KEY: u16 = 1;
48/// `EV_REL`: movimiento relativo. Es lo que emite un mouse y el nodo «Mouse» de
49/// un touchpad.
50const EV_REL: u16 = 2;
51const REL_X: u16 = 0;
52const REL_Y: u16 = 1;
53/// Los tres botones, con los códigos de `input-event-codes.h` (`BTN_LEFT` =
54/// 0x110). Están en el mismo espacio que las teclas, y por eso `tecla()` los
55/// descarta: pasarlos por el mapa del teclado daría una letra cualquiera.
56const BTN_LEFT: u16 = 0x110;
57const BTN_RIGHT: u16 = 0x111;
58const BTN_MIDDLE: u16 = 0x112;
59
60/// Un botón del puntero.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Boton {
63 Izquierdo,
64 Derecho,
65 Medio,
66}
67
68/// Lo que llegó de los aparatos de entrada, en el orden en que llegó.
69///
70/// Una sola cola para teclado y puntero, porque es **una sola cola en el
71/// kernel**: un mouse emite sus botones como `EV_KEY` en el mismo nodo que su
72/// movimiento, y separarlos en dos lectores haría que uno le robara los eventos
73/// al otro.
74#[derive(Debug, Clone)]
75pub enum Evento {
76 Tecla(KeyEvent),
77 /// El puntero quedó en esa posición, **en píxeles de la pantalla**. Se emite
78 /// **uno por lectura**, no uno por evento del kernel: un mouse manda decenas
79 /// de `EV_REL` por gesto y repintar en cada uno no mueve el cursor más
80 /// suave, sólo gasta cuadros.
81 Puntero { x: f32, y: f32 },
82 /// Un botón, con la posición donde estaba el puntero en ese momento.
83 Boton { boton: Boton, apretado: bool, x: f32, y: f32 },
84}
85
86/// Todos los aparatos de entrada de la máquina, abiertos y listos para leer.
87pub struct Entradas {
88 dispositivos: Vec<std::fs::File>,
89 /// Cuáles ya están abiertos, por nombre de nodo (`event3`). Sin esto,
90 /// refrescar los abriría de nuevo en cada pasada.
91 abiertos: alloc_set::Set,
92 mods: Modifiers,
93 /// Dónde está el puntero, en píxeles. Arranca en el centro: un cursor en
94 /// `(0,0)` se lee como que no hay cursor.
95 x: f32,
96 y: f32,
97 /// El tamaño de la pantalla, para no dejar salir el cursor. Un cursor que se
98 /// va del borde no vuelve: el movimiento es relativo y no hay nada que lo
99 /// traiga de nuevo.
100 ancho: f32,
101 alto: f32,
102 /// ¿Alguno de los aparatos abiertos emite movimiento relativo?
103 ///
104 /// Sale de `/sys/class/input/<nodo>/device/capabilities/rel`, que es un
105 /// **archivo** —no un `ioctl`— y trae la máscara de ejes. Hace falta saberlo
106 /// *antes* de que alguien mueva el mouse: la pantalla tiene que poder decir
107 /// con qué se maneja, y decir «tocá» donde no hay puntero es mandar a hacer
108 /// algo imposible.
109 hay_puntero: bool,
110}
111
112/// Un conjunto de cadenas sin traer nada: son diez nombres cortos y el crate no
113/// tiene dependencias fuera de las de la casa.
114mod alloc_set {
115 #[derive(Default)]
116 pub struct Set(Vec<String>);
117 impl Set {
118 pub fn contiene(&self, s: &str) -> bool {
119 self.0.iter().any(|x| x == s)
120 }
121 pub fn meter(&mut self, s: String) {
122 self.0.push(s);
123 }
124 pub fn cuantos(&self) -> usize {
125 self.0.len()
126 }
127 pub fn nombres(&self) -> &[String] {
128 &self.0
129 }
130 }
131}
132
133impl Entradas {
134 /// Abre lo que haya. **No falla si no hay ninguno**: una máquina sin teclado
135 /// es un caso real —un servidor, una prueba automática— y lo que corresponde
136 /// es seguir pintando, no negarse a arrancar.
137 ///
138 /// El tamaño de la pantalla es para acotar el cursor; con `(0, 0)` el
139 /// puntero no se mueve de su sitio, que es lo correcto cuando quien llama
140 /// sólo quiere enumerar los aparatos.
141 pub fn abrir(ancho: u16, alto: u16) -> Self {
142 let mut t = Entradas {
143 dispositivos: Vec::new(),
144 abiertos: alloc_set::Set::default(),
145 mods: Modifiers::default(),
146 x: ancho as f32 / 2.0,
147 y: alto as f32 / 2.0,
148 ancho: ancho as f32,
149 alto: alto as f32,
150 hay_puntero: false,
151 };
152 t.refrescar();
153 t
154 }
155
156 /// ¿Hay con qué apuntar? Ver [`Entradas::hay_puntero`].
157 pub fn tiene_puntero(&self) -> bool {
158 self.hay_puntero
159 }
160
161 /// Dónde está el puntero ahora, en píxeles.
162 pub fn puntero(&self) -> (f32, f32) {
163 (self.x, self.y)
164 }
165
166 /// Abre lo que haya aparecido desde la última vez. Devuelve cuántos nuevos.
167 ///
168 /// **Existe porque los dispositivos no están todos al arrancar.** Un teclado
169 /// USB enumera de forma asíncrona y tarda segundos; el del portátil puede
170 /// registrarse después que el video. Con un solo escaneo al inicio, la
171 /// pantalla queda pintada y sorda para siempre — y desde afuera se ve igual
172 /// que una máquina colgada. Medido en metal el 2026-09-29: el instalador
173 /// dibujó los discos y no respondió a ninguna tecla, mientras la consola de
174 /// texto de la otra pantalla sí las recibía.
175 pub fn refrescar(&mut self) -> usize {
176 self.refrescar_en("/dev/input")
177 }
178
179 /// Como [`Self::refrescar`] pero sobre otro directorio. Es lo que permite
180 /// probarlo sin `/dev`: un directorio con archivos `eventN` se comporta
181 /// igual para lo que acá importa, que es **cuáles se abren y cuáles no**.
182 pub fn refrescar_en(&mut self, raiz: &str) -> usize {
183 let mut nuevos = 0;
184 let dispositivos = &mut self.dispositivos;
185 let abiertos = &mut self.abiertos;
186 let hay_puntero = &mut self.hay_puntero;
187 if let Ok(dir) = std::fs::read_dir(raiz) {
188 for e in dir.filter_map(|e| e.ok()) {
189 let nombre = e.file_name();
190 let Some(n) = nombre.to_str() else { continue };
191 if !n.starts_with("event") || abiertos.contiene(n) {
192 continue;
193 }
194 let Ok(f) = std::fs::File::open(e.path()) else { continue };
195 // Sin bloquear: la pantalla tiene que poder repintarse aunque
196 // nadie toque nada.
197 unsafe {
198 let fd = f.as_raw_fd();
199 let flags = libc_fcntl_getfl(fd);
200 if flags >= 0 {
201 libc_fcntl_setfl(fd, flags | O_NONBLOCK);
202 }
203 }
204 dispositivos.push(f);
205 // ¿Este aparato apunta? Se pregunta al abrirlo y no cuando
206 // llega el primer movimiento: la pantalla tiene que poder decir
207 // con qué se maneja **antes** de que nadie toque nada.
208 if apunta(n) {
209 *hay_puntero = true;
210 }
211 abiertos.meter(n.to_string());
212 nuevos += 1;
213 }
214 }
215 nuevos
216 }
217
218 /// Cuántos dispositivos de entrada hay abiertos, y cómo se llaman.
219 ///
220 /// **El conteo importa y el nombre más.** `vacio()` sólo dice que hay
221 /// *alguno*, y en cualquier portátil siempre hay alguno —el botón de
222 /// encendido, la tapa— así que «hay teclado» era verdad y no servía: el
223 /// teclado podía no estar. El nombre sale de
224 /// `/sys/class/input/<nodo>/device/name`, que es un archivo, no un ioctl.
225 pub fn descripcion(&self) -> Vec<String> {
226 self.abiertos
227 .nombres()
228 .iter()
229 .map(|n| {
230 let ruta = format!("/sys/class/input/{n}/device/name");
231 match std::fs::read_to_string(&ruta) {
232 Ok(s) => format!("{n} ({})", s.trim()),
233 Err(_) => n.clone(),
234 }
235 })
236 .collect()
237 }
238
239 /// Cuántos hay abiertos.
240 pub fn cuantos(&self) -> usize {
241 self.abiertos.cuantos()
242 }
243
244 /// `true` si no se pudo abrir ninguno. Quien llame decide qué hacer — y
245 /// **decirlo** es mejor que una pantalla que no responde sin explicación.
246 pub fn vacio(&self) -> bool {
247 self.dispositivos.is_empty()
248 }
249
250 /// Lo que llegó desde la última vez. Lista vacía = nadie tocó nada, que es
251 /// el caso normal.
252 ///
253 /// **El movimiento se junta en uno solo.** Un gesto de mouse son decenas de
254 /// `EV_REL`; emitir uno por cada uno haría un cuadro por cada uno, y el
255 /// cursor no se mueve más suave por eso — se mueve igual y con la pantalla
256 /// ocupada. Los botones sí salen uno por uno, y cada uno lleva la posición
257 /// que el puntero tenía **en ese momento**: si el gesto fue mover-y-clickear
258 /// dentro de la misma lectura, el click no puede resolverse contra la
259 /// posición anterior.
260 pub fn leer(&mut self) -> Vec<Evento> {
261 let mut fuera = Vec::new();
262 let (x0, y0) = (self.x, self.y);
263 let mut buf = [0u8; TAM_EVENTO * 16];
264 for d in &mut self.dispositivos {
265 let Ok(n) = d.read(&mut buf) else { continue };
266 for trozo in buf[..n].chunks_exact(TAM_EVENTO) {
267 let tipo = u16::from_ne_bytes([trozo[16], trozo[17]]);
268 let codigo = u16::from_ne_bytes([trozo[18], trozo[19]]);
269 let valor = i32::from_ne_bytes([trozo[20], trozo[21], trozo[22], trozo[23]]);
270 if tipo == EV_REL {
271 // Acotado a la pantalla: el movimiento es relativo, así que
272 // un cursor que se va del borde no vuelve nunca.
273 match codigo {
274 REL_X => self.x = (self.x + valor as f32).clamp(0.0, self.ancho - 1.0),
275 REL_Y => self.y = (self.y + valor as f32).clamp(0.0, self.alto - 1.0),
276 // La rueda queda sin usar a propósito: hoy ninguna
277 // pantalla de esta cara tiene nada que rodar, y un
278 // evento que nadie consume es una promesa sin cumplir.
279 _ => {}
280 }
281 continue;
282 }
283 if tipo != EV_KEY {
284 continue;
285 }
286 // 0 suelta, 1 pulsa, 2 repetición automática. La repetición se
287 // ignora: una tecla sostenida recorrería un menú entero.
288 let estado = match valor {
289 0 => KeyState::Released,
290 1 => KeyState::Pressed,
291 _ => continue,
292 };
293 if let Some(b) = boton(codigo) {
294 fuera.push(Evento::Boton {
295 boton: b,
296 apretado: estado == KeyState::Pressed,
297 x: self.x,
298 y: self.y,
299 });
300 continue;
301 }
302 if let Some(m) = modificador(codigo) {
303 aplicar_modificador(&mut self.mods, m, estado == KeyState::Pressed);
304 continue;
305 }
306 let Some(key) = tecla(codigo, self.mods.shift) else { continue };
307 // `text` es lo que un campo inserta cuando la tecla produce
308 // texto. Dejarlo en `None` no rompe la navegación —las flechas
309 // no producen nada— pero deja mudo cualquier campo donde alguien
310 // tenga que escribir, que acá es el que pide el nombre del
311 // disco antes de formatearlo.
312 let text = match &key {
313 Key::Character(s) => Some(s.clone()),
314 Key::Named(_) => None,
315 };
316 fuera.push(Evento::Tecla(KeyEvent {
317 key,
318 state: estado,
319 text,
320 modifiers: self.mods,
321 repeat: false,
322 }));
323 }
324 }
325 // El movimiento, una vez y al final.
326 if self.x != x0 || self.y != y0 {
327 fuera.push(Evento::Puntero { x: self.x, y: self.y });
328 }
329 fuera
330 }
331}
332
333/// ¿Este nodo de `/dev/input` emite movimiento relativo en los dos ejes?
334///
335/// Se lee de `/sys/class/input/<nodo>/device/capabilities/rel`, que trae la
336/// máscara de ejes en hexadecimal. Es un **archivo**: el `ioctl` equivalente
337/// (`EVIOCGBIT`) obligaría a traerse `libc` y a escribir `unsafe` por un dato
338/// que el kernel ya publica como texto.
339///
340/// Se exige `REL_X` **y** `REL_Y` (bits 0 y 1, máscara `3`): hay aparatos que
341/// declaran sólo la rueda —el `SEM USB Keyboard Consumer Control` de la máquina
342/// del operador declara `1040`— y ésos no son punteros.
343fn apunta(nodo: &str) -> bool {
344 apunta_en("/sys/class/input", nodo)
345}
346
347fn apunta_en(raiz: &str, nodo: &str) -> bool {
348 let ruta = format!("{raiz}/{nodo}/device/capabilities/rel");
349 let Ok(txt) = std::fs::read_to_string(&ruta) else { return false };
350 // La máscara puede venir en varias palabras separadas por espacios, de la
351 // más alta a la más baja; los ejes 0 y 1 están en la última.
352 let Some(ultima) = txt.split_whitespace().last() else { return false };
353 let Ok(bits) = u64::from_str_radix(ultima, 16) else { return false };
354 bits & 0b11 == 0b11
355}
356
357/// Del código del kernel al botón. Los tres primeros y nada más: un mouse de
358/// juego manda ocho y los que no tienen significado acá no se inventan.
359fn boton(codigo: u16) -> Option<Boton> {
360 match codigo {
361 BTN_LEFT => Some(Boton::Izquierdo),
362 BTN_RIGHT => Some(Boton::Derecho),
363 BTN_MIDDLE => Some(Boton::Medio),
364 _ => None,
365 }
366}
367
368enum Mod {
369 Shift,
370 Ctrl,
371 Alt,
372}
373
374fn modificador(codigo: u16) -> Option<Mod> {
375 match codigo {
376 42 | 54 => Some(Mod::Shift),
377 29 | 97 => Some(Mod::Ctrl),
378 56 | 100 => Some(Mod::Alt),
379 _ => None,
380 }
381}
382
383fn aplicar_modificador(m: &mut Modifiers, cual: Mod, apretado: bool) {
384 match cual {
385 Mod::Shift => m.shift = apretado,
386 Mod::Ctrl => m.ctrl = apretado,
387 Mod::Alt => m.alt = apretado,
388 }
389}
390
391/// Del código del kernel a la tecla del modelo.
392///
393/// Los números son los de `input-event-codes.h` y **no cambian**: son ABI del
394/// kernel desde hace veinte años. Por eso van acá y no en un archivo de
395/// configuración que alguien tendría que mantener.
396///
397/// Es un teclado **US sin mapa de teclado**: alcanza para navegar, para escribir
398/// el nombre de un dispositivo y para confirmar. Un `xkb` completo es otro
399/// trabajo, y meterlo acá arrastraría lo que este camino existe para no
400/// arrastrar.
401fn tecla(codigo: u16, shift: bool) -> Option<Key> {
402 let nombrada = |n: NamedKey| Some(Key::Named(n));
403 match codigo {
404 1 => nombrada(NamedKey::Escape),
405 14 => nombrada(NamedKey::Backspace),
406 15 => nombrada(NamedKey::Tab),
407 28 | 96 => nombrada(NamedKey::Enter),
408 103 => nombrada(NamedKey::ArrowUp),
409 108 => nombrada(NamedKey::ArrowDown),
410 105 => nombrada(NamedKey::ArrowLeft),
411 106 => nombrada(NamedKey::ArrowRight),
412 102 => nombrada(NamedKey::Home),
413 107 => nombrada(NamedKey::End),
414 111 => nombrada(NamedKey::Delete),
415 57 => Some(Key::Character(" ".into())),
416 _ => caracter(codigo, shift).map(|c| Key::Character(c.to_string())),
417 }
418}
419
420fn caracter(codigo: u16, shift: bool) -> Option<char> {
421 const FILA1: &str = "1234567890-=";
422 const FILA1S: &str = "!@#$%^&*()_+";
423 const FILA2: &str = "qwertyuiop[]";
424 const FILA3: &str = "asdfghjkl;'";
425 const FILA4: &str = "zxcvbnm,./";
426 let de = |s: &str, i: usize| s.chars().nth(i);
427 let c = match codigo {
428 2..=13 => de(if shift { FILA1S } else { FILA1 }, codigo as usize - 2),
429 16..=27 => de(FILA2, codigo as usize - 16),
430 30..=40 => de(FILA3, codigo as usize - 30),
431 44..=53 => de(FILA4, codigo as usize - 44),
432 _ => None,
433 }?;
434 Some(if shift { c.to_ascii_uppercase() } else { c })
435}
436
437// Las dos llamadas a `fcntl` que hacen falta, sin traerse el crate `libc`
438// entero por dos constantes: este crate va adentro de un initramfs y cada
439// dependencia se paga en bytes.
440const O_NONBLOCK: i32 = 0o4000;
441const F_GETFL: i32 = 3;
442const F_SETFL: i32 = 4;
443
444unsafe extern "C" {
445 fn fcntl(fd: i32, cmd: i32, ...) -> i32;
446}
447
448unsafe fn libc_fcntl_getfl(fd: i32) -> i32 {
449 unsafe { fcntl(fd, F_GETFL) }
450}
451
452unsafe fn libc_fcntl_setfl(fd: i32, flags: i32) -> i32 {
453 unsafe { fcntl(fd, F_SETFL, flags) }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn las_flechas_y_el_enter_son_lo_que_dicen() {
462 // Son los códigos de `input-event-codes.h`, que son ABI del kernel.
463 assert_eq!(tecla(103, false), Some(Key::Named(NamedKey::ArrowUp)));
464 assert_eq!(tecla(108, false), Some(Key::Named(NamedKey::ArrowDown)));
465 assert_eq!(tecla(28, false), Some(Key::Named(NamedKey::Enter)));
466 assert_eq!(tecla(1, false), Some(Key::Named(NamedKey::Escape)));
467 }
468
469 #[test]
470 fn se_puede_escribir_lo_que_pide_la_confirmacion_fuerte() {
471 // `dev sda` con los códigos del kernel: d-e-v-s-d-a. Si alguna no
472 // saliera, el campo donde hay que escribir el nombre del disco sería
473 // imposible de completar — y esa confirmación existe justamente para
474 // los discos de los que no se sabe qué tienen.
475 let escrito: String = [32u16, 18, 47, 31, 32, 30]
476 .iter()
477 .filter_map(|c| match tecla(*c, false) {
478 Some(Key::Character(s)) => s.chars().next(),
479 _ => None,
480 })
481 .collect();
482 assert_eq!(escrito, "devsda");
483 }
484
485 #[test]
486 fn el_shift_cambia_la_fila_de_numeros_y_no_solo_las_letras() {
487 assert_eq!(caracter(2, false), Some('1'));
488 assert_eq!(caracter(2, true), Some('!'));
489 assert_eq!(caracter(16, false), Some('q'));
490 assert_eq!(caracter(16, true), Some('Q'));
491 }
492
493 #[test]
494 fn una_tecla_que_no_conozco_no_inventa_un_caracter() {
495 // Mejor perder una tecla rara que meter basura en un campo donde alguien
496 // está escribiendo el nombre del disco que va a formatear.
497 assert_eq!(tecla(0, false), None);
498 assert_eq!(tecla(200, false), None);
499 }
500}
501
502#[cfg(test)]
503mod tests_escaneo {
504 use super::*;
505
506 fn tmp(nombre: &str) -> std::path::PathBuf {
507 let d = std::env::temp_dir().join(format!("llimphi-drm-{}-{nombre}", std::process::id()));
508 let _ = std::fs::remove_dir_all(&d);
509 std::fs::create_dir_all(&d).unwrap();
510 d
511 }
512
513 /// **El caso que costó un viaje al metal.** Los dispositivos no están todos
514 /// al arrancar: un teclado USB enumera de forma asíncrona. Con un solo
515 /// escaneo al inicio, la pantalla queda pintada y sorda para siempre.
516 #[test]
517 fn toma_los_que_aparecen_despues() {
518 let d = tmp("tarde");
519 std::fs::write(d.join("event0"), b"").unwrap();
520 let mut t = Entradas {
521 dispositivos: Vec::new(),
522 abiertos: alloc_set::Set::default(),
523 mods: Modifiers::default(),
524 x: 0.0,
525 y: 0.0,
526 ancho: 100.0,
527 alto: 100.0,
528 hay_puntero: false,
529 };
530 assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1);
531
532 // Aparece el teclado, tarde.
533 std::fs::write(d.join("event1"), b"").unwrap();
534 assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1, "el nuevo se toma");
535 assert_eq!(t.cuantos(), 2);
536
537 // Y no se reabre lo que ya estaba: si no, cada pasada duplicaría los
538 // descriptores hasta quedarse sin ninguno.
539 assert_eq!(t.refrescar_en(d.to_str().unwrap()), 0);
540 assert_eq!(t.cuantos(), 2);
541 let _ = std::fs::remove_dir_all(&d);
542 }
543
544 /// Sólo los nodos de evento. En `/dev/input` también viven `mice`, `mouse0`
545 /// y el directorio `by-path`, y abrirlos como si fueran teclados da basura.
546 #[test]
547 fn solo_los_nodos_de_evento() {
548 let d = tmp("filtro");
549 for n in ["event0", "mice", "mouse0", "js0"] {
550 std::fs::write(d.join(n), b"").unwrap();
551 }
552 let mut t = Entradas {
553 dispositivos: Vec::new(),
554 abiertos: alloc_set::Set::default(),
555 mods: Modifiers::default(),
556 x: 0.0,
557 y: 0.0,
558 ancho: 100.0,
559 alto: 100.0,
560 hay_puntero: false,
561 };
562 assert_eq!(t.refrescar_en(d.to_str().unwrap()), 1);
563 // Se mira el NODO y no el texto completo: `descripcion()` le pide el
564 // nombre al sysfs de la máquina donde corre, y ahí `event0` es lo que
565 // sea que tenga esa máquina («Lid Switch» en la primera donde se probó).
566 // Lo que este test fija es el filtro, no el nombre.
567 let d0 = t.descripcion();
568 assert_eq!(d0.len(), 1);
569 assert!(d0[0].starts_with("event0"), "{:?}", d0);
570 let _ = std::fs::remove_dir_all(&d);
571 }
572}
573
574#[cfg(test)]
575mod pruebas_del_puntero {
576 use super::*;
577 use std::io::Write;
578
579 /// Arma un `struct input_event` como el que escribe el kernel.
580 fn ev(tipo: u16, codigo: u16, valor: i32) -> [u8; TAM_EVENTO] {
581 let mut b = [0u8; TAM_EVENTO];
582 b[16..18].copy_from_slice(&tipo.to_ne_bytes());
583 b[18..20].copy_from_slice(&codigo.to_ne_bytes());
584 b[20..24].copy_from_slice(&valor.to_ne_bytes());
585 b
586 }
587
588 /// Un lector sobre un archivo con eventos ya escritos. Es el mismo camino
589 /// que `/dev/input`: `read` de un fd y decodificar — lo único que cambia es
590 /// quién escribió los bytes.
591 fn con_eventos(nombre: &str, eventos: &[[u8; TAM_EVENTO]]) -> Entradas {
592 let d = std::env::temp_dir()
593 .join(format!("llimphi-drm-pt-{}-{nombre}", std::process::id()));
594 let _ = std::fs::remove_dir_all(&d);
595 std::fs::create_dir_all(&d).unwrap();
596 let mut f = std::fs::File::create(d.join("event0")).unwrap();
597 for e in eventos {
598 f.write_all(e).unwrap();
599 }
600 drop(f);
601 let mut t = Entradas {
602 dispositivos: Vec::new(),
603 abiertos: alloc_set::Set::default(),
604 mods: Modifiers::default(),
605 x: 50.0,
606 y: 50.0,
607 ancho: 100.0,
608 alto: 100.0,
609 hay_puntero: false,
610 };
611 t.refrescar_en(d.to_str().unwrap());
612 let _ = std::fs::remove_dir_all(&d);
613 t
614 }
615
616 /// 🔴 **El movimiento se junta en UNO.** Un gesto de mouse son decenas de
617 /// `EV_REL`; un cuadro por cada uno no mueve el cursor más suave, sólo deja
618 /// la pantalla ocupada.
619 #[test]
620 fn muchos_movimientos_dan_un_solo_evento() {
621 let mut t = con_eventos(
622 "junta",
623 &[
624 ev(EV_REL, REL_X, 3),
625 ev(EV_REL, REL_Y, 4),
626 ev(EV_REL, REL_X, 3),
627 ev(EV_REL, REL_Y, 4),
628 ],
629 );
630 let salida = t.leer();
631 assert_eq!(salida.len(), 1, "un gesto, un evento: {salida:?}");
632 match salida[0] {
633 Evento::Puntero { x, y } => assert_eq!((x, y), (56.0, 58.0)),
634 ref otro => panic!("esperaba un movimiento: {otro:?}"),
635 }
636 }
637
638 /// 🔴 **El click lleva la posición de ESE momento.** Si el gesto fue
639 /// mover-y-clickear dentro de la misma lectura, resolver el click contra la
640 /// posición anterior lo pondría en otro lado — y en esta pantalla «otro
641 /// lado» es otra partición.
642 #[test]
643 fn el_boton_usa_la_posicion_del_momento() {
644 let mut t = con_eventos(
645 "clic",
646 &[ev(EV_REL, REL_X, 10), ev(EV_KEY, BTN_LEFT, 1), ev(EV_REL, REL_X, 10)],
647 );
648 let salida = t.leer();
649 let apretado = salida
650 .iter()
651 .find_map(|e| match e {
652 Evento::Boton { boton: Boton::Izquierdo, apretado: true, x, y } => Some((*x, *y)),
653 _ => None,
654 })
655 .expect("tiene que haber un botón apretado");
656 assert_eq!(apretado, (60.0, 50.0), "la posición del click es la de ese instante");
657 // Y el movimiento final, una sola vez y con el total.
658 let movio = salida
659 .iter()
660 .filter_map(|e| match e {
661 Evento::Puntero { x, y } => Some((*x, *y)),
662 _ => None,
663 })
664 .collect::<Vec<_>>();
665 assert_eq!(movio, vec![(70.0, 50.0)]);
666 }
667
668 /// El cursor **no se va de la pantalla**: el movimiento es relativo y lo que
669 /// sale por el borde no vuelve nunca.
670 #[test]
671 fn el_cursor_no_se_escapa_por_el_borde() {
672 let mut t = con_eventos("borde", &[ev(EV_REL, REL_X, -500), ev(EV_REL, REL_Y, 500)]);
673 t.leer();
674 assert_eq!(t.puntero(), (0.0, 99.0), "acotado a la pantalla de 100x100");
675 }
676
677 /// Los botones **no pasan por el mapa del teclado**. Están en el mismo
678 /// espacio de códigos que las teclas, así que sin filtrarlos un click
679 /// escribiría una letra en el campo donde se tipea el nombre del disco.
680 #[test]
681 fn un_click_no_escribe_una_letra() {
682 let mut t = con_eventos("noletra", &[ev(EV_KEY, BTN_LEFT, 1)]);
683 let salida = t.leer();
684 assert!(
685 !salida.iter().any(|e| matches!(e, Evento::Tecla(_))),
686 "un botón no es una tecla: {salida:?}"
687 );
688 assert_eq!(tecla(BTN_LEFT, false), None, "y el mapa del teclado no lo conoce");
689 }
690
691 /// Y una tecla sigue siendo una tecla: el brazo de control de lo de arriba.
692 #[test]
693 fn una_tecla_sigue_llegando_como_tecla() {
694 let mut t = con_eventos("tecla", &[ev(EV_KEY, 28, 1)]);
695 let salida = t.leer();
696 assert!(
697 matches!(&salida[..], [Evento::Tecla(k)] if k.key == Key::Named(NamedKey::Enter)),
698 "{salida:?}"
699 );
700 }
701
702 /// 🔴 **Declarar sólo la rueda NO es ser un puntero.** El
703 /// `SEM USB Keyboard Consumer Control` de la máquina del operador declara
704 /// `REL=1040` —rueda y nada más— y tomarlo por mouse haría que la pantalla
705 /// prometa un cursor que no existe.
706 #[test]
707 fn la_rueda_sola_no_cuenta_como_puntero() {
708 let d = std::env::temp_dir().join(format!("llimphi-drm-cap-{}", std::process::id()));
709 let _ = std::fs::remove_dir_all(&d);
710 for (nodo, mascara) in [("event0", "3"), ("event1", "1040"), ("event2", "0")] {
711 let dir = d.join(nodo).join("device").join("capabilities");
712 std::fs::create_dir_all(&dir).unwrap();
713 std::fs::write(dir.join("rel"), mascara).unwrap();
714 }
715 let raiz = d.to_str().unwrap();
716 assert!(apunta_en(raiz, "event0"), "REL_X|REL_Y sí es un puntero");
717 assert!(!apunta_en(raiz, "event1"), "sólo la rueda no");
718 assert!(!apunta_en(raiz, "event2"), "y un teclado tampoco");
719 assert!(!apunta_en(raiz, "event9"), "ni un nodo que no existe");
720 let _ = std::fs::remove_dir_all(&d);
721 }
722}