tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-drm/src/entrada.rs Rust · 722 líneas · 28.8 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}