gioser

tawasuyu/brahman

Rust 570TOML 215Markdown 68JSON 198261 Definitions

⎇ b31f98883379a172efd309cd2cec98d16eb591db crates/apps/mirada-compositor/src/main.rs Rust · 727 lines · 25.5 KB History Who touched what raw

feat(mirada): API de acciones — mirada-ctl + HUD interactivo b31f98883 · sergio ·

1//! `mirada-compositor` — el Cuerpo del compositor carmen.
2//!
3//! Un compositor Wayland teselante real, sobre `smithay`, con backend
4//! `winit`: corre **anidado** como una ventana dentro de tu sesión
5//! gráfica actual (X11 o Wayland). Habla el protocolo Wayland con los
6//! clientes, compone sus superficies y aplica la geometría que decide el
7//! Cerebro.
8//!
9//! Dos modos:
10//!
11//! - **Autónomo** (por defecto): lleva un [`Desktop`] embebido — es un
12//!   compositor teselante completo en un solo proceso. Lánzalo y abre
13//!   clientes; el teclado (`Super+…`) maneja el escritorio.
14//! - **Enlazado** (`MIRADA_SOCKET=/ruta`): el Cuerpo escucha ahí y la
15//!   app `mirada` (el Cerebro GPUI) se conecta; la geometría viaja por
16//!   [`mirada_link`].
17//!
18//! Cómo probarlo en un Linux real: ver `crates/apps/mirada-compositor/README.md`.
19
20use std::sync::Arc;
21use std::time::Instant;
22
23use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
24use smithay::backend::renderer::element::surface::{
25    render_elements_from_surface_tree, WaylandSurfaceRenderElement,
26};
27use smithay::backend::renderer::element::Kind;
28use smithay::backend::renderer::gles::GlesRenderer;
29use smithay::backend::renderer::utils::{draw_render_elements, on_commit_buffer_handler};
30use smithay::backend::renderer::{Color32F, Frame, Renderer};
31use smithay::backend::winit::{self, WinitEvent};
32use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
33use smithay::input::{Seat, SeatHandler, SeatState};
34use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
35use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
36use smithay::reexports::wayland_server::protocol::wl_buffer;
37use smithay::reexports::wayland_server::protocol::wl_seat;
38use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
39use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
40use smithay::reexports::winit::platform::pump_events::PumpStatus;
41use smithay::utils::{Rectangle, SERIAL_COUNTER};
42use smithay::utils::{Serial, Transform};
43use smithay::wayland::buffer::BufferHandler;
44use smithay::wayland::compositor::{
45    with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
46    CompositorState, SurfaceAttributes, TraversalAction,
47};
48use smithay::wayland::selection::data_device::{
49    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
50};
51use smithay::wayland::selection::SelectionHandler;
52use smithay::wayland::shell::xdg::{
53    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
54    XdgToplevelSurfaceData,
55};
56use smithay::wayland::shm::{ShmHandler, ShmState};
57use smithay::{
58    delegate_compositor, delegate_data_device, delegate_seat, delegate_shm, delegate_xdg_shell,
59};
60
61use mirada_body::{BodyOp, BodyState};
62use mirada_brain::{BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap};
63use mirada_link::BodyLink;
64
65// ---------------------------------------------------------------------
66// Estado
67// ---------------------------------------------------------------------
68
69/// De dónde salen las decisiones de geometría.
70enum Brain {
71    /// El compositor lleva su propio `Desktop` — proceso único.
72    Embedded(Desktop),
73    /// Un Cerebro externo (la app `mirada`) por socket.
74    Linked(BodyLink),
75}
76
77/// Una ventana de cliente que el compositor gestiona.
78struct ManagedWindow {
79    id: u64,
80    toplevel: ToplevelSurface,
81    surface: WlSurface,
82    /// Esquina superior-izquierda en píxeles, según el Cerebro.
83    loc: (i32, i32),
84    visible: bool,
85}
86
87/// El estado global del compositor.
88struct App {
89    compositor_state: CompositorState,
90    xdg_shell_state: XdgShellState,
91    shm_state: ShmState,
92    seat_state: SeatState<Self>,
93    data_device_state: DataDeviceState,
94    seat: Seat<Self>,
95    keyboard: Option<KeyboardHandle<Self>>,
96
97    /// Ventanas gestionadas, en orden de aparición.
98    windows: Vec<ManagedWindow>,
99    /// La contabilidad del Cuerpo (mirada-body).
100    body: BodyState,
101    /// El Cerebro: embebido o enlazado.
102    brain: Brain,
103    /// Atajos globales a interceptar (los registra el Cerebro).
104    grabs: Vec<String>,
105    /// Atajo capturado en el último evento de teclado, pendiente de enviar.
106    pending_keybind: Option<String>,
107    next_id: u64,
108    running: bool,
109}
110
111impl App {
112    /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
113    fn brain_feed(&mut self, event: BodyEvent) {
114        let cmds = match &mut self.brain {
115            Brain::Embedded(desktop) => desktop.on_event(event),
116            Brain::Linked(link) => {
117                let _ = link.send(&event);
118                Vec::new()
119            }
120        };
121        self.apply_commands(cmds);
122    }
123
124    /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
125    fn brain_poll(&mut self) {
126        let cmds = match &self.brain {
127            Brain::Linked(link) => link.drain(),
128            Brain::Embedded(_) => Vec::new(),
129        };
130        if !cmds.is_empty() {
131            self.apply_commands(cmds);
132        }
133    }
134
135    /// Atiende una petición del API de control (`mirada-ctl`).
136    fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
137        match req {
138            CtlRequest::Do(action) => {
139                let cmds = match &mut self.brain {
140                    Brain::Embedded(d) => Some(d.apply(action)),
141                    Brain::Linked(_) => None,
142                };
143                match cmds {
144                    Some(cmds) => {
145                        self.apply_commands(cmds);
146                        CtlReply::Ok
147                    }
148                    None => CtlReply::Error(
149                        "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
150                    ),
151                }
152            }
153            CtlRequest::ListWindows => match &self.brain {
154                Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
155                Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
156            },
157        }
158    }
159
160    /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
161    fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
162        for cmd in cmds {
163            let ops = self.body.apply(cmd);
164            for op in ops {
165                self.exec_op(op);
166            }
167        }
168    }
169
170    /// Ejecuta una operación concreta sobre las superficies reales.
171    fn exec_op(&mut self, op: BodyOp) {
172        match op {
173            BodyOp::Configure { id, rect, visible } => {
174                if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
175                    w.loc = (rect.x, rect.y);
176                    w.visible = visible;
177                    w.toplevel.with_pending_state(|s| {
178                        s.size = Some((rect.w.max(1), rect.h.max(1)).into());
179                    });
180                    w.toplevel.send_pending_configure();
181                }
182            }
183            BodyOp::Focus(id) => {
184                let mut target = None;
185                for w in &self.windows {
186                    let active = w.id == id;
187                    if active {
188                        target = Some(w.surface.clone());
189                    }
190                    w.toplevel.with_pending_state(|s| {
191                        if active {
192                            s.states.set(xdg_toplevel::State::Activated);
193                        } else {
194                            s.states.unset(xdg_toplevel::State::Activated);
195                        }
196                    });
197                    w.toplevel.send_pending_configure();
198                }
199                if let Some(kb) = self.keyboard.clone() {
200                    kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
201                }
202            }
203            BodyOp::Unfocus => {
204                if let Some(kb) = self.keyboard.clone() {
205                    kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
206                }
207            }
208            BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
209                if let Some(w) = self.windows.iter().find(|w| w.id == id) {
210                    w.toplevel.send_close();
211                }
212            }
213            BodyOp::SetGrabs(keys) => self.grabs = keys,
214            BodyOp::SetCursor(_) => {}
215            BodyOp::Shutdown => self.running = false,
216        }
217    }
218
219    /// Registra un toplevel recién creado y avisa al Cerebro.
220    fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
221        let surface = toplevel.wl_surface().clone();
222        let id = self.next_id;
223        self.next_id += 1;
224
225        let (app_id, title) = with_states(&surface, |states| {
226            states
227                .data_map
228                .get::<XdgToplevelSurfaceData>()
229                .and_then(|d| d.lock().ok())
230                .map(|d| {
231                    (
232                        d.app_id.clone().unwrap_or_default(),
233                        d.title.clone().unwrap_or_default(),
234                    )
235                })
236                .unwrap_or_default()
237        });
238        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
239        let title = if title.is_empty() { format!("ventana {id}") } else { title };
240
241        self.windows.push(ManagedWindow {
242            id,
243            toplevel,
244            surface,
245            loc: (0, 0),
246            visible: false,
247        });
248        let ev = self.body.open_surface(id, app_id, title);
249        self.brain_feed(ev);
250    }
251}
252
253// ---------------------------------------------------------------------
254// Handlers de protocolo
255// ---------------------------------------------------------------------
256
257impl CompositorHandler for App {
258    fn compositor_state(&mut self) -> &mut CompositorState {
259        &mut self.compositor_state
260    }
261
262    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
263        &client.get_data::<ClientState>().unwrap().compositor_state
264    }
265
266    fn commit(&mut self, surface: &WlSurface) {
267        on_commit_buffer_handler::<Self>(surface);
268    }
269}
270
271impl BufferHandler for App {
272    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
273}
274
275impl ShmHandler for App {
276    fn shm_state(&self) -> &ShmState {
277        &self.shm_state
278    }
279}
280
281impl XdgShellHandler for App {
282    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
283        &mut self.xdg_shell_state
284    }
285
286    fn new_toplevel(&mut self, surface: ToplevelSurface) {
287        surface.with_pending_state(|s| {
288            s.states.set(xdg_toplevel::State::Activated);
289        });
290        surface.send_configure();
291        self.register_toplevel(surface);
292    }
293
294    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
295        let pos = self
296            .windows
297            .iter()
298            .position(|w| w.surface == *surface.wl_surface());
299        if let Some(pos) = pos {
300            let id = self.windows.remove(pos).id;
301            if let Some(ev) = self.body.close_surface(id) {
302                self.brain_feed(ev);
303            }
304        }
305    }
306
307    fn title_changed(&mut self, surface: ToplevelSurface) {
308        let id = self
309            .windows
310            .iter()
311            .find(|w| w.surface == *surface.wl_surface())
312            .map(|w| w.id);
313        let Some(id) = id else { return };
314        let title = with_states(surface.wl_surface(), |states| {
315            states
316                .data_map
317                .get::<XdgToplevelSurfaceData>()
318                .and_then(|d| d.lock().ok())
319                .and_then(|d| d.title.clone())
320                .unwrap_or_default()
321        });
322        if let Some(ev) = self.body.retitle_surface(id, title) {
323            self.brain_feed(ev);
324        }
325    }
326
327    fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
328        let _ = surface.send_configure();
329    }
330
331    fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
332
333    fn reposition_request(
334        &mut self,
335        _surface: PopupSurface,
336        _positioner: PositionerState,
337        _token: u32,
338    ) {
339    }
340}
341
342impl SelectionHandler for App {
343    type SelectionUserData = ();
344}
345
346impl DataDeviceHandler for App {
347    fn data_device_state(&self) -> &DataDeviceState {
348        &self.data_device_state
349    }
350}
351impl ClientDndGrabHandler for App {}
352impl ServerDndGrabHandler for App {
353    fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
354}
355
356impl SeatHandler for App {
357    type KeyboardFocus = WlSurface;
358    type PointerFocus = WlSurface;
359    type TouchFocus = WlSurface;
360
361    fn seat_state(&mut self) -> &mut SeatState<Self> {
362        &mut self.seat_state
363    }
364
365    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
366    fn cursor_image(
367        &mut self,
368        _seat: &Seat<Self>,
369        _image: smithay::input::pointer::CursorImageStatus,
370    ) {
371    }
372}
373
374delegate_compositor!(App);
375delegate_xdg_shell!(App);
376delegate_shm!(App);
377delegate_seat!(App);
378delegate_data_device!(App);
379
380// ---------------------------------------------------------------------
381// Datos por cliente
382// ---------------------------------------------------------------------
383
384#[derive(Default)]
385struct ClientState {
386    compositor_state: CompositorClientState,
387}
388impl ClientData for ClientState {
389    fn initialized(&self, _id: ClientId) {}
390    fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
391}
392
393// ---------------------------------------------------------------------
394// Utilidades
395// ---------------------------------------------------------------------
396
397/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
398/// modificadores y el keysym, con el mismo formato que el mapa de teclas
399/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
400fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
401    let utf = xkb::keysym_to_utf8(sym);
402    let key = utf.trim_end_matches('\0');
403    let name = if key == " " {
404        "space".to_string()
405    } else {
406        let mut chars = key.chars();
407        let c = chars.next()?;
408        if chars.next().is_some() || !c.is_ascii_graphic() {
409            return None;
410        }
411        c.to_ascii_lowercase().to_string()
412    };
413    let mut combo = String::new();
414    if mods.logo {
415        combo.push_str("Super+");
416    }
417    if mods.ctrl {
418        combo.push_str("Ctrl+");
419    }
420    if mods.shift {
421        combo.push_str("Shift+");
422    }
423    if mods.alt {
424        combo.push_str("Alt+");
425    }
426    combo.push_str(&name);
427    Some(combo)
428}
429
430/// Despacha los callbacks de frame de un árbol de superficies: avisa a
431/// cada cliente de que puede dibujar el siguiente cuadro.
432fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
433    with_surface_tree_downward(
434        surface,
435        (),
436        |_, _, &()| TraversalAction::DoChildren(()),
437        |_surf, states, &()| {
438            for callback in states
439                .cached_state
440                .get::<SurfaceAttributes>()
441                .current()
442                .frame_callbacks
443                .drain(..)
444            {
445                callback.done(time);
446            }
447        },
448        |_, _, &()| true,
449    );
450}
451
452// ---------------------------------------------------------------------
453// Bucle principal
454// ---------------------------------------------------------------------
455
456fn run() -> Result<(), Box<dyn std::error::Error>> {
457    let mut display: Display<App> = Display::new()?;
458    let dh = display.handle();
459
460    let mut seat_state = SeatState::new();
461    let seat = seat_state.new_wl_seat(&dh, "mirada");
462
463    // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
464    // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
465    let keymap_path = Keymap::default_path();
466
467    // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
468    let brain = match std::env::var("MIRADA_SOCKET") {
469        Ok(path) => {
470            println!("mirada-compositor · esperando al Cerebro en {path} …");
471            let link = BodyLink::listen(&path)?;
472            println!("mirada-compositor · Cerebro conectado.");
473            Brain::Linked(link)
474        }
475        Err(_) => {
476            println!("mirada-compositor · modo autónomo (Cerebro embebido).");
477            let keymap = match &keymap_path {
478                Some(p) => Keymap::load_or_init(p),
479                None => Keymap::default(),
480            };
481            Brain::Embedded(Desktop::with_keymap(keymap))
482        }
483    };
484
485    let mut state = App {
486        compositor_state: CompositorState::new::<App>(&dh),
487        xdg_shell_state: XdgShellState::new::<App>(&dh),
488        shm_state: ShmState::new::<App>(&dh, Vec::new()),
489        seat_state,
490        data_device_state: DataDeviceState::new::<App>(&dh),
491        seat,
492        keyboard: None,
493        windows: Vec::new(),
494        body: BodyState::new(),
495        brain,
496        grabs: Vec::new(),
497        pending_keybind: None,
498        next_id: 1,
499        running: true,
500    };
501
502    let keyboard = state.seat.add_keyboard(Default::default(), 200, 25)?;
503    state.keyboard = Some(keyboard.clone());
504
505    // En modo embebido, el propio Desktop dicta los atajos a interceptar.
506    if let Brain::Embedded(desktop) = &state.brain {
507        let grab = desktop.grab_keys();
508        state.apply_commands(vec![grab]);
509    }
510
511    // Vigilancia del keymap para recargarlo en caliente — sólo tiene
512    // sentido con el Cerebro embebido.
513    let keymap_watch = match (&state.brain, &keymap_path) {
514        (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
515        _ => None,
516    };
517    if keymap_watch.is_some() {
518        println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
519    }
520
521    // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
522    // externo, el socket de control lo abre él.
523    let ctl = match &state.brain {
524        Brain::Embedded(_) => {
525            let path = mirada_brain::ctl::default_socket_path();
526            match CtlServer::bind(&path) {
527                Ok(s) => {
528                    println!("mirada-compositor · API de control en {}", path.display());
529                    Some(s)
530                }
531                Err(e) => {
532                    eprintln!("mirada-compositor · sin API de control: {e}");
533                    None
534                }
535            }
536        }
537        Brain::Linked(_) => None,
538    };
539
540    // El backend gráfico va primero. winit abre la ventana del compositor
541    // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
542    // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
543    // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
544    // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
545    // y se quedaría colgado para siempre.
546    let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
547        Ok(pair) => pair,
548        Err(e) => {
549            eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
550            eprintln!(
551                "   El backend `winit` necesita una sesión gráfica anfitriona\n   \
552                 (X11 o Wayland) donde dibujar la ventana del compositor.\n   \
553                 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n   \
554                 XDG_SESSION_TYPE='{}'.\n   \
555                 Lánzalo desde un escritorio gráfico, o desde un servidor X\n   \
556                 virtual (Xvfb) al que te conectes por VNC.",
557                std::env::var("DISPLAY").unwrap_or_default(),
558                std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
559                std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
560            );
561            return Err(e.into());
562        }
563    };
564
565    // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
566    // publica *después* de winit, sólo para los clientes que lancemos
567    // como procesos hijos.
568    let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
569    let socket_name = listener
570        .socket_name()
571        .and_then(|s| s.to_str())
572        .unwrap_or("wayland-?")
573        .to_string();
574    std::env::set_var("WAYLAND_DISPLAY", &socket_name);
575    println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
576    println!("   lanza un cliente:  WAYLAND_DISPLAY={socket_name} foot");
577
578    let start = Instant::now();
579    let mut clients = Vec::new();
580
581    // Salida inicial = el tamaño de la ventana winit.
582    {
583        let size = backend.window_size();
584        let ev = state.body.add_output(0, size.w, size.h);
585        state.brain_feed(ev);
586    }
587
588    while state.running {
589        // 1 · Eventos del backend (teclado, redimensión, cierre).
590        let status = winit.dispatch_new_events(|event| match event {
591            WinitEvent::CloseRequested => state.running = false,
592            WinitEvent::Resized { size, .. } => {
593                let ev = state.body.remove_output(0);
594                state.brain_feed(ev);
595                let ev = state.body.add_output(0, size.w, size.h);
596                state.brain_feed(ev);
597            }
598            WinitEvent::Input(InputEvent::Keyboard { event }) => {
599                let code = event.key_code();
600                let key_state = event.state();
601                let pressed = key_state == KeyState::Pressed;
602                let time = start.elapsed().as_millis() as u32;
603                keyboard.clone().input::<(), _>(
604                    &mut state,
605                    code,
606                    key_state,
607                    SERIAL_COUNTER.next_serial(),
608                    time,
609                    |st, mods, handle| {
610                        if !pressed {
611                            return FilterResult::Forward;
612                        }
613                        if let Some(combo) = combo_string(mods, handle.modified_sym()) {
614                            if st.grabs.contains(&combo) {
615                                st.pending_keybind = Some(combo);
616                                return FilterResult::Intercept(());
617                            }
618                        }
619                        FilterResult::Forward
620                    },
621                );
622                if let Some(combo) = state.pending_keybind.take() {
623                    let ev = state.body.keybind(combo);
624                    state.brain_feed(ev);
625                }
626            }
627            _ => {}
628        });
629        if let PumpStatus::Exit(_) = status {
630            break;
631        }
632
633        // 2 · Comandos de un Cerebro enlazado.
634        state.brain_poll();
635
636        // 2 bis · Recarga del keymap si el archivo cambió en disco.
637        if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
638            if let Some(path) = &keymap_path {
639                match Keymap::load(path) {
640                    Ok(km) => {
641                        let cmd = if let Brain::Embedded(d) = &mut state.brain {
642                            Some(d.set_keymap(km))
643                        } else {
644                            None
645                        };
646                        if let Some(cmd) = cmd {
647                            state.apply_commands(vec![cmd]);
648                        }
649                        println!("mirada-compositor · keymap recargado.");
650                    }
651                    Err(e) => eprintln!(
652                        "mirada-compositor · keymap inválido, conservo el anterior: {e}"
653                    ),
654                }
655            }
656        }
657
658        // 2 ter · Peticiones del API de control (mirada-ctl).
659        if let Some(ctl) = &ctl {
660            while let Some(mut conn) = ctl.poll() {
661                let reply = match conn.read_request() {
662                    Ok(Some(req)) => state.serve_ctl(req),
663                    Ok(None) => continue,
664                    Err(e) => CtlReply::Error(format!("{e}")),
665                };
666                let _ = conn.reply(&reply);
667            }
668        }
669
670        // 3 · Composición de las superficies en sus rectángulos.
671        let size = backend.window_size();
672        let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
673        {
674            let (renderer, mut framebuffer) = backend.bind().unwrap();
675            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = state
676                .windows
677                .iter()
678                .filter(|w| w.visible)
679                .flat_map(|w| {
680                    render_elements_from_surface_tree(
681                        renderer,
682                        &w.surface,
683                        w.loc,
684                        1.0,
685                        1.0,
686                        Kind::Unspecified,
687                    )
688                })
689                .collect();
690            let mut frame = renderer
691                .render(&mut framebuffer, size, Transform::Flipped180)
692                .unwrap();
693            frame
694                .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
695                .unwrap();
696            draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
697            let _ = frame.finish().unwrap();
698        }
699
700        // 4 · Callbacks de frame + clientes nuevos + flush.
701        let time = start.elapsed().as_millis() as u32;
702        for w in &state.windows {
703            send_frames_surface_tree(&w.surface, time);
704        }
705        if let Some(stream) = listener.accept()? {
706            let client = display
707                .handle()
708                .insert_client(stream, Arc::new(ClientState::default()))
709                .unwrap();
710            clients.push(client);
711        }
712        display.dispatch_clients(&mut state)?;
713        display.flush_clients()?;
714
715        backend.submit(Some(&[damage])).unwrap();
716    }
717
718    println!("mirada-compositor · adiós.");
719    Ok(())
720}
721
722fn main() {
723    if let Err(e) = run() {
724        eprintln!("mirada-compositor · error: {e}");
725        std::process::exit(1);
726    }
727}