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-jaula/nahual-jailed/src/main.rs Rust · 228 líneas · 10.2 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//! **El explorador adentro de una jaula: nahual entero corriendo en otro proceso.**
2//!
3//! Es el hermano de `nada_enjaulado`, y existe por el mismo motivo: cinco fases
4//! de piezas probadas por separado no contestan si al juntarse pasa algo. Acá el
5//! **mismo** `nahual-shell-llimphi` que corre suelto en el escritorio compone
6//! hacia un cable en vez de hacia una ventana.
7//!
8//! ```bash
9//! cargo run --manifest-path 02_ruway/llimphi/llimphi-jaula/Cargo.toml \
10//!     --no-default-features --features std --example nahual_enjaulado -- <socket> [carpeta]
11//! ```
12//!
13//! Normalmente lo lanza el anfitrión, que le pasa el socket.
14//!
15//! ## Lo que este archivo NO hace, y es el punto
16//!
17//! No reimplementa nada del explorador. No hay una vista, ni un `update`, ni un
18//! mapeo de teclas propios: se llaman los de la lib, que son **los mismos** que
19//! usa el binario de escritorio. Si esto tuviera lógica de explorador adentro,
20//! todo el trabajo de cruzar los treinta y dos crates habría sido al pedo — el
21//! objetivo es que la app se vea **igual** enjaulada que suelta.
22//!
23//! Lo único propio son las tres piezas de la frontera: el medidor (pregunta en
24//! vez de shapear), el despachador (una cola que el bucle drena, donde el
25//! escritorio tiene el bucle de winit) y el bucle.
26//!
27//! ## Con qué arranca, y por qué importa decirlo
28//!
29//! Sin `terminal` ni `svg`: el terminal embebido trae wgpu y el visor de
30//! vectores rinde contra el rasterizador del escritorio, así que ninguno de los
31//! dos puede cruzar todavía. Un `.svg` cae al visor de texto y F4 no abre nada.
32//! Es una degradación **nombrada**, no un agujero: lo que se ve es el explorador
33//! completo menos esas dos cosas.
34
35use std::cell::RefCell;
36use std::collections::VecDeque;
37use std::rc::Rc;
38use std::sync::{Arc, Mutex};
39
40use llimphi_cable::{DelAnfitrion, Huesped};
41use llimphi_jaula::{medidor, Jaula};
42use nahual_shell_llimphi as nahual;
43
44/// El despachador del huésped: una **cola** que el bucle drena.
45///
46/// En el escritorio, el explorador le manda sus efectos al `Handle` de winit.
47/// Acá no hay ventana: la cola es ésta, y el bucle la vacía en cada vuelta. Para
48/// el explorador no cambia nada — habla con el trait, no con quien lo cumple.
49///
50/// `Arc<Mutex<…>>` y no `Rc<RefCell<…>>` porque el trait pide `Send + Sync`: el
51/// trabajo de fondo (una miniatura, un censo de disco) corre en un hilo de
52/// verdad, igual que en el escritorio.
53#[derive(Clone, Default)]
54struct Cola(Arc<Mutex<VecDeque<nahual::Msg>>>);
55
56impl llimphi_despacho::Despachador<nahual::Msg> for Cola {
57    fn despachar(&self, msg: nahual::Msg) {
58        // Si el mutex está envenenado, un efecto perdido no justifica tirar el
59        // explorador abajo: se descarta y se sigue. El síntoma sería una
60        // miniatura que no aparece, no una pantalla negra.
61        if let Ok(mut c) = self.0.lock() {
62            c.push_back(msg);
63        }
64    }
65
66    /// El trabajo pesado va a un hilo, como en el escritorio. El default del
67    /// trait lo correría inline y bloquearía el fotograma — un censo de disco
68    /// adentro del `view` congelaría la ventana del anfitrión, no la propia.
69    fn lanzar(&self, trabajo: Box<dyn FnOnce() -> nahual::Msg + Send + 'static>) {
70        let cola = self.clone();
71        std::thread::spawn(move || {
72            llimphi_despacho::Despachador::despachar(&cola, trabajo());
73        });
74    }
75}
76
77impl Cola {
78    fn sacar(&self) -> Option<nahual::Msg> {
79        self.0.lock().ok()?.pop_front()
80    }
81}
82
83fn main() {
84    let mut args = std::env::args().skip(1);
85    let Some(ruta) = args.next() else {
86        eprintln!("uso: nahual_enjaulado <ruta-del-socket> [carpeta]");
87        std::process::exit(2);
88    };
89    let cwd = args
90        .next()
91        .map(std::path::PathBuf::from)
92        .filter(|p| p.is_dir())
93        .or_else(|| std::env::current_dir().ok())
94        .unwrap_or_else(|| std::path::PathBuf::from("/"));
95
96    let flujo = match std::os::unix::net::UnixStream::connect(&ruta) {
97        Ok(f) => f,
98        Err(e) => {
99            eprintln!("nahual enjaulado: no pude conectarme a {ruta}: {e}");
100            std::process::exit(1);
101        }
102    };
103
104    // El cable se comparte entre el medidor —que corre ADENTRO de `fotograma`—
105    // y el bucle, que manda el cuadro después. Un solo hilo: las dos tomas
106    // nunca se anidan.
107    let cable = Rc::new(RefCell::new(Huesped::new(flujo)));
108
109    let medidor_cable = Rc::clone(&cable);
110    let (mut ancho, mut alto) = nahual::TAMANO_INICIAL;
111    let mut jaula = Jaula::new(ancho as f32, alto as f32, move |p| {
112        // El marshalling es el compartido con el camino de wawa: si el huésped
113        // armara su propia petición, un campo nuevo del estilo quedaría sin
114        // viajar de un solo lado y el texto se mediría distinto según quién
115        // hospeda.
116        medidor::medir_con(p, |peticion, salida| {
117            let Ok(pedido) = postcard::from_bytes(peticion) else { return -1 };
118            match medidor_cable.borrow_mut().medir(&pedido) {
119                Ok(m) => {
120                    salida.copy_from_slice(&m.a_bytes());
121                    0
122                }
123                Err(_) => -1,
124            }
125        })
126        .unwrap_or(llimphi_text::wire::Medida { width: 0.0, full_width: 0.0, height: 0.0 })
127    });
128
129    // **El explorador de verdad.** El watcher de config va en `None`: vigilar el
130    // archivo es un efecto del escritorio, y el anfitrión ya tiene el suyo.
131    let mut modelo = nahual::build_model(cwd, llimphi_theme::Theme::dark(), None);
132    let cola = Cola::default();
133    let despacho: nahual::Despacho = Arc::new(cola.clone());
134
135    loop {
136        // 1) Componer con la vista de nahual —la misma que pinta el binario
137        //    suelto— y mandar el cuadro.
138        let montado = jaula.fotograma(nahual::vista(&modelo));
139        // `escena_para_enviar` y no `escena`: lleva los píxeles de las imágenes
140        // que el anfitrión todavía no tiene, y **sólo ésos**. Sin los píxeles el
141        // anfitrión no puede dibujar ninguna imagen (las cuenta y sigue), que es
142        // el «todo negro» del reporte; mandándolos todos en cada cuadro serían
143        // megabytes por fotograma para pintar lo mismo.
144        if cable.borrow_mut().enviar_cuadro(jaula.escena_para_enviar()).is_err() {
145            return; // el anfitrión se fue
146        }
147
148        // 2) Esperar. Un bucle que compusiera sin parar quemaría un núcleo para
149        //    pintar siempre lo mismo.
150        let evento = match cable.borrow_mut().esperar_evento() {
151            Ok(e) => e,
152            Err(_) => return,
153        };
154
155        let mut pendientes: Vec<nahual::Msg> = Vec::new();
156        match evento {
157            // El hit-test lo hace el huésped contra SU layout: el anfitrión
158            // manda una coordenada, qué hay debajo lo sabe la app.
159            DelAnfitrion::Puntero { x, y, apretado } if apretado => {
160                if let Some(msg) = jaula.click(&montado, x, y) {
161                    pendientes.push(msg);
162                }
163            }
164            DelAnfitrion::Tecla(ev) => {
165                // Las teclas las enruta nahual, con la MISMA función que usa el
166                // binario de escritorio. Acá no hay un mapeo paralelo.
167                if let Some(msg) = nahual::enrutar_tecla(&modelo, &ev) {
168                    pendientes.push(msg);
169                }
170            }
171            DelAnfitrion::Rueda(d) => {
172                if d.y != 0.0 {
173                    pendientes.push(nahual::Msg::ScrollLines(d.y));
174                }
175            }
176            DelAnfitrion::Tamano { ancho: a, alto: h } => {
177                ancho = a as u32;
178                alto = h as u32;
179                jaula.redimensionar(a, h);
180                pendientes.push(nahual::Msg::Resized(a, h));
181            }
182            // El explorador no tiene mensaje de preedición: sus campos de texto
183            // son los del widget compartido, que hoy recibe las teclas ya
184            // resueltas. Cuando lo tenga, va acá — y hasta entonces tragar el
185            // evento es correcto: reenviarlo como tecla perdería la preedición,
186            // que es justo lo que el carril aparte existe para no perder.
187            DelAnfitrion::Ime(_) => {}
188            // **El anfitrión dice a dónde ir.** Es el clic del árbol de carpetas
189            // que vive en SU sidebar: sin esto ese árbol es decoración, porque
190            // mueve el estado del anfitrión y el que se ve es éste.
191            DelAnfitrion::Ir(ruta) => {
192                pendientes.push(nahual::Msg::TreeSelect(std::path::PathBuf::from(ruta)));
193            }
194            // **Y las otras dos secciones del sidebar del anfitrión.** Un disco
195            // y una colección NO son carpetas: son fuentes sintéticas con su
196            // propio espacio de ids, así que no caben en `Ir`. El `Msg` lo arma
197            // la lib del explorador y no este bucle — acá no hay un segundo
198            // parser de ids que se pueda despegar del primero.
199            DelAnfitrion::AbrirDispositivo(id) => {
200                pendientes.push(nahual::msg_abrir_dispositivo(&id));
201            }
202            DelAnfitrion::AbrirColeccion(id) => {
203                // Un id que no parsea se descarta y se dice: abrir «alguna»
204                // colección sería peor que no abrir ninguna.
205                match nahual::msg_abrir_coleccion(&id) {
206                    Some(m) => pendientes.push(m),
207                    None => eprintln!("nahual-jailed: id de colección ilegible: {id}"),
208                }
209            }
210            DelAnfitrion::Adios => return,
211            DelAnfitrion::Puntero { .. } | DelAnfitrion::PunteroSalio => {}
212            // La medida se contesta adentro del medidor; si llega acá es que
213            // sobró una, y descartarla es lo correcto.
214            DelAnfitrion::Medida(_) => {}
215        }
216
217        // 3) Aplicar lo del evento **y todo lo que los efectos hayan encolado**.
218        //    El drenaje va después y en bucle porque un `update` puede despachar
219        //    otro `Msg` —una miniatura que terminó, un censo que reporta— y
220        //    dejarlo para la vuelta siguiente sería mandar un cuadro ya viejo.
221        for msg in pendientes {
222            modelo = nahual::update(modelo, msg, &despacho);
223        }
224        while let Some(msg) = cola.sacar() {
225            modelo = nahual::update(modelo, msg, &despacho);
226        }
227    }
228}