gioser

tawasuyu/brahman

Rust 476TOML 190Markdown 50JSON 176602 Definitions

⎇ 8cd34bf17311d89f73e89de51c8a61ce203ca453 crates/apps/mirada-compositor/src/main.rs Rust · 884 lines · 31.2 KB History Who touched what raw

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