gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

⎇ fb2f3a2d015088a9caa5041588fe3ae86a52f971 crates/apps/mirada-compositor/src/main.rs Rust · 843 líneas · 29.8 KB Historia Quién tocó qué crudo

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