gioser

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114552 Sutichaykuna

⎇ 5ede927d3432c0c895e863b39ca7eb5f73b78213 crates/apps/mirada-compositor/src/main.rs Rust · 1055 siq'ikuna · 38.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(mirada): sesión de escritorio — autostart y conmutación de VT 5ede927d3 · 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::solid::SolidColorBuffer;
28use smithay::backend::renderer::element::Kind;
29use smithay::backend::renderer::gles::GlesRenderer;
30use smithay::backend::renderer::utils::{
31    draw_render_elements, on_commit_buffer_handler, with_renderer_surface_state,
32};
33use smithay::backend::renderer::{Color32F, Frame, Renderer};
34use smithay::backend::winit::{self, WinitEvent};
35use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
36use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData, PointerHandle};
37use smithay::input::{Seat, SeatHandler, SeatState};
38use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
39use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
40use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
41use smithay::reexports::wayland_server::protocol::wl_buffer;
42use smithay::reexports::wayland_server::protocol::wl_output;
43use smithay::reexports::wayland_server::protocol::wl_seat;
44use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
45use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
46use smithay::reexports::winit::platform::pump_events::PumpStatus;
47use smithay::utils::{Rectangle, SERIAL_COUNTER};
48use smithay::utils::{Serial, Transform};
49use smithay::wayland::buffer::BufferHandler;
50use smithay::wayland::compositor::{
51    with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
52    CompositorState, SurfaceAttributes, TraversalAction,
53};
54use smithay::wayland::selection::data_device::{
55    ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
56};
57use smithay::wayland::selection::SelectionHandler;
58use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
59use smithay::wayland::shell::xdg::{
60    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
61    XdgToplevelSurfaceData,
62};
63use smithay::wayland::output::OutputHandler;
64use smithay::wayland::shm::{ShmHandler, ShmState};
65use smithay::{
66    delegate_compositor, delegate_data_device, delegate_output, delegate_seat, delegate_shm,
67    delegate_xdg_decoration, delegate_xdg_shell,
68};
69
70use mirada_body::{BodyOp, BodyState};
71use mirada_brain::{
72    BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
73};
74use mirada_link::BodyLink;
75
76mod drm_backend;
77
78// ---------------------------------------------------------------------
79// Estado
80// ---------------------------------------------------------------------
81
82/// De dónde salen las decisiones de geometría.
83enum Brain {
84    /// El compositor lleva su propio `Desktop` — proceso único.
85    Embedded(Desktop),
86    /// Un Cerebro externo (la app `mirada`) por socket.
87    Linked(BodyLink),
88}
89
90/// Una ventana de cliente que el compositor gestiona.
91struct ManagedWindow {
92    id: u64,
93    toplevel: ToplevelSurface,
94    surface: WlSurface,
95    /// Esquina superior-izquierda de la celda asignada, según el Cerebro.
96    loc: (i32, i32),
97    /// Tamaño de la celda asignada — para centrar la ventana si el
98    /// cliente presenta una superficie más pequeña.
99    size: (i32, i32),
100    visible: bool,
101    /// `true` si flota: se compone por encima de las teseladas.
102    floating: bool,
103    /// `true` si tiene el foco del teclado — pinta el marco resaltado.
104    focused: bool,
105    /// Búferes de los 4 lados del marco (arriba, abajo, izq., der.) —
106    /// cada uno con su `Id` estable para el seguimiento de daño.
107    borders: [SolidColorBuffer; 4],
108}
109
110/// Un arrastre de ratón en curso: mueve o redimensiona una ventana.
111struct DragGrab {
112    /// La ventana que se arrastra.
113    id: u64,
114    /// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho).
115    mode: DragMode,
116    /// Posición del puntero al empezar el arrastre.
117    start_pointer: (f64, f64),
118    /// Rectángulo `(x, y, w, h)` de la ventana al empezar.
119    start_rect: (i32, i32, i32, i32),
120}
121
122/// Qué le hace un arrastre a la ventana.
123#[derive(Clone, Copy)]
124enum DragMode {
125    /// Reubicar la ventana — la esquina la sigue al puntero.
126    Move,
127    /// Redimensionarla — la esquina inferior-derecha sigue al puntero.
128    Resize,
129}
130
131/// El estado global del compositor.
132struct App {
133    compositor_state: CompositorState,
134    xdg_shell_state: XdgShellState,
135    shm_state: ShmState,
136    seat_state: SeatState<Self>,
137    data_device_state: DataDeviceState,
138    seat: Seat<Self>,
139    keyboard: Option<KeyboardHandle<Self>>,
140    pointer: Option<PointerHandle<Self>>,
141    /// Posición del puntero en coordenadas globales.
142    pointer_loc: (f64, f64),
143    /// Qué cursor pide el cliente enfocado — una superficie suya, un
144    /// cursor con nombre, u oculto. El backend lo pinta en consecuencia.
145    cursor_status: CursorImageStatus,
146    /// Arrastre de ventana en curso (mover o redimensionar con el ratón).
147    drag: Option<DragGrab>,
148
149    /// Ventanas gestionadas, en orden de aparición.
150    windows: Vec<ManagedWindow>,
151    /// La contabilidad del Cuerpo (mirada-body).
152    body: BodyState,
153    /// El Cerebro: embebido o enlazado.
154    brain: Brain,
155    /// Atajos globales a interceptar (los registra el Cerebro).
156    grabs: Vec<String>,
157    /// Atajo capturado en el último evento de teclado, pendiente de enviar.
158    pending_keybind: Option<String>,
159    next_id: u64,
160    running: bool,
161}
162
163impl App {
164    /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
165    fn brain_feed(&mut self, event: BodyEvent) {
166        let cmds = match &mut self.brain {
167            Brain::Embedded(desktop) => desktop.on_event(event),
168            Brain::Linked(link) => {
169                let _ = link.send(&event);
170                Vec::new()
171            }
172        };
173        self.apply_commands(cmds);
174    }
175
176    /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
177    fn brain_poll(&mut self) {
178        let cmds = match &self.brain {
179            Brain::Linked(link) => link.drain(),
180            Brain::Embedded(_) => Vec::new(),
181        };
182        if !cmds.is_empty() {
183            self.apply_commands(cmds);
184        }
185    }
186
187    /// Atiende una petición del API de control (`mirada-ctl`).
188    fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
189        match req {
190            CtlRequest::Do(action) => {
191                let cmds = match &mut self.brain {
192                    Brain::Embedded(d) => Some(d.apply(action)),
193                    Brain::Linked(_) => None,
194                };
195                match cmds {
196                    Some(cmds) => {
197                        self.apply_commands(cmds);
198                        CtlReply::Ok
199                    }
200                    None => CtlReply::Error(
201                        "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
202                    ),
203                }
204            }
205            CtlRequest::ListWindows => match &self.brain {
206                Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
207                Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
208            },
209        }
210    }
211
212    /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
213    fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
214        for cmd in cmds {
215            let ops = self.body.apply(cmd);
216            for op in ops {
217                self.exec_op(op);
218            }
219        }
220    }
221
222    /// Ejecuta una operación concreta sobre las superficies reales.
223    fn exec_op(&mut self, op: BodyOp) {
224        match op {
225            BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
226                if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
227                    w.loc = (rect.x, rect.y);
228                    w.size = (rect.w, rect.h);
229                    w.visible = visible;
230                    w.floating = floating;
231                    w.toplevel.with_pending_state(|s| {
232                        s.size = Some((rect.w.max(1), rect.h.max(1)).into());
233                        if fullscreen {
234                            s.states.set(xdg_toplevel::State::Fullscreen);
235                        } else {
236                            s.states.unset(xdg_toplevel::State::Fullscreen);
237                        }
238                    });
239                    w.toplevel.send_pending_configure();
240                }
241            }
242            BodyOp::Focus(id) => {
243                let mut target = None;
244                for w in &mut self.windows {
245                    let active = w.id == id;
246                    w.focused = active;
247                    if active {
248                        target = Some(w.surface.clone());
249                    }
250                    w.toplevel.with_pending_state(|s| {
251                        if active {
252                            s.states.set(xdg_toplevel::State::Activated);
253                        } else {
254                            s.states.unset(xdg_toplevel::State::Activated);
255                        }
256                    });
257                    w.toplevel.send_pending_configure();
258                }
259                if let Some(kb) = self.keyboard.clone() {
260                    kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
261                }
262            }
263            BodyOp::Unfocus => {
264                for w in &mut self.windows {
265                    w.focused = false;
266                }
267                if let Some(kb) = self.keyboard.clone() {
268                    kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
269                }
270            }
271            BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
272                if let Some(w) = self.windows.iter().find(|w| w.id == id) {
273                    w.toplevel.send_close();
274                }
275            }
276            BodyOp::SetGrabs(keys) => self.grabs = keys,
277            BodyOp::SetCursor(_) => {}
278            BodyOp::Spawn(cmd) => spawn_command(&cmd),
279            BodyOp::Shutdown => self.running = false,
280        }
281    }
282
283    /// Registra un toplevel recién creado y avisa al Cerebro.
284    fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
285        let surface = toplevel.wl_surface().clone();
286        let id = self.next_id;
287        self.next_id += 1;
288
289        let (app_id, title) = with_states(&surface, |states| {
290            states
291                .data_map
292                .get::<XdgToplevelSurfaceData>()
293                .and_then(|d| d.lock().ok())
294                .map(|d| {
295                    (
296                        d.app_id.clone().unwrap_or_default(),
297                        d.title.clone().unwrap_or_default(),
298                    )
299                })
300                .unwrap_or_default()
301        });
302        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
303        let title = if title.is_empty() { format!("ventana {id}") } else { title };
304
305        self.windows.push(ManagedWindow {
306            id,
307            toplevel,
308            surface,
309            loc: (0, 0),
310            size: (0, 0),
311            visible: false,
312            floating: false,
313            focused: false,
314            borders: std::array::from_fn(|_| SolidColorBuffer::default()),
315        });
316        let ev = self.body.open_surface(id, app_id, title);
317        self.brain_feed(ev);
318    }
319}
320
321// ---------------------------------------------------------------------
322// Handlers de protocolo
323// ---------------------------------------------------------------------
324
325impl CompositorHandler for App {
326    fn compositor_state(&mut self) -> &mut CompositorState {
327        &mut self.compositor_state
328    }
329
330    fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
331        &client.get_data::<ClientState>().unwrap().compositor_state
332    }
333
334    fn commit(&mut self, surface: &WlSurface) {
335        on_commit_buffer_handler::<Self>(surface);
336    }
337}
338
339impl BufferHandler for App {
340    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
341}
342
343impl ShmHandler for App {
344    fn shm_state(&self) -> &ShmState {
345        &self.shm_state
346    }
347}
348
349impl XdgShellHandler for App {
350    fn xdg_shell_state(&mut self) -> &mut XdgShellState {
351        &mut self.xdg_shell_state
352    }
353
354    fn new_toplevel(&mut self, surface: ToplevelSurface) {
355        surface.with_pending_state(|s| {
356            s.states.set(xdg_toplevel::State::Activated);
357        });
358        surface.send_configure();
359        self.register_toplevel(surface);
360    }
361
362    fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
363        let pos = self
364            .windows
365            .iter()
366            .position(|w| w.surface == *surface.wl_surface());
367        if let Some(pos) = pos {
368            let id = self.windows.remove(pos).id;
369            if let Some(ev) = self.body.close_surface(id) {
370                self.brain_feed(ev);
371            }
372        }
373    }
374
375    fn title_changed(&mut self, surface: ToplevelSurface) {
376        let id = self
377            .windows
378            .iter()
379            .find(|w| w.surface == *surface.wl_surface())
380            .map(|w| w.id);
381        let Some(id) = id else { return };
382        let title = with_states(surface.wl_surface(), |states| {
383            states
384                .data_map
385                .get::<XdgToplevelSurfaceData>()
386                .and_then(|d| d.lock().ok())
387                .and_then(|d| d.title.clone())
388                .unwrap_or_default()
389        });
390        if let Some(ev) = self.body.retitle_surface(id, title) {
391            self.brain_feed(ev);
392        }
393    }
394
395    fn fullscreen_request(
396        &mut self,
397        surface: ToplevelSurface,
398        _output: Option<wl_output::WlOutput>,
399    ) {
400        let id = self
401            .windows
402            .iter()
403            .find(|w| w.surface == *surface.wl_surface())
404            .map(|w| w.id);
405        if let Some(id) = id {
406            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
407        }
408    }
409
410    fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
411        let id = self
412            .windows
413            .iter()
414            .find(|w| w.surface == *surface.wl_surface())
415            .map(|w| w.id);
416        if let Some(id) = id {
417            self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
418        }
419    }
420
421    fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
422        let _ = surface.send_configure();
423    }
424
425    fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
426
427    fn reposition_request(
428        &mut self,
429        _surface: PopupSurface,
430        _positioner: PositionerState,
431        _token: u32,
432    ) {
433    }
434}
435
436/// Decoración de ventana: carmen tesela, así que las ventanas no llevan
437/// barra de título. Le decimos a todo cliente que la decoración la pone
438/// el servidor (`ServerSide`) — y como el servidor no dibuja ninguna, la
439/// ventana queda sin marco. Sin esto, clientes como `foot` se dibujan su
440/// propia barra (CSD), que estorba en un escritorio teselante.
441impl XdgDecorationHandler for App {
442    fn new_decoration(&mut self, toplevel: ToplevelSurface) {
443        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
444        toplevel.send_configure();
445    }
446
447    fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
448        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
449        toplevel.send_configure();
450    }
451
452    fn unset_mode(&mut self, toplevel: ToplevelSurface) {
453        toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
454        toplevel.send_configure();
455    }
456}
457
458impl SelectionHandler for App {
459    type SelectionUserData = ();
460}
461
462impl DataDeviceHandler for App {
463    fn data_device_state(&self) -> &DataDeviceState {
464        &self.data_device_state
465    }
466}
467impl ClientDndGrabHandler for App {}
468impl ServerDndGrabHandler for App {
469    fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
470}
471
472impl SeatHandler for App {
473    type KeyboardFocus = WlSurface;
474    type PointerFocus = WlSurface;
475    type TouchFocus = WlSurface;
476
477    fn seat_state(&mut self) -> &mut SeatState<Self> {
478        &mut self.seat_state
479    }
480
481    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
482
483    /// El cliente enfocado pidió un cursor — guardamos su petición; el
484    /// backend la pinta (su superficie, o el cuadrado si es con nombre).
485    fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
486        self.cursor_status = image;
487    }
488}
489
490/// El protocolo `wl_output` no necesita estado propio — basta con
491/// anunciar el global para que los clientes vean que hay un monitor.
492impl OutputHandler for App {}
493
494delegate_compositor!(App);
495delegate_xdg_shell!(App);
496delegate_xdg_decoration!(App);
497delegate_shm!(App);
498delegate_seat!(App);
499delegate_data_device!(App);
500delegate_output!(App);
501
502// ---------------------------------------------------------------------
503// Datos por cliente
504// ---------------------------------------------------------------------
505
506#[derive(Default)]
507struct ClientState {
508    compositor_state: CompositorClientState,
509}
510impl ClientData for ClientState {
511    fn initialized(&self, _id: ClientId) {}
512    fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
513}
514
515// ---------------------------------------------------------------------
516// Utilidades
517// ---------------------------------------------------------------------
518
519/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
520/// modificadores y el keysym, con el mismo formato que el mapa de teclas
521/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
522fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
523    let utf = xkb::keysym_to_utf8(sym);
524    let key = utf.trim_end_matches('\0');
525    let name = if key == " " {
526        "space".to_string()
527    } else {
528        // ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
529        let mut chars = key.chars();
530        match (chars.next(), chars.next()) {
531            (Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
532            // Si no, una tecla con nombre: Return, Tab, Up, F5…
533            _ => named_key(sym)?,
534        }
535    };
536    let mut combo = String::new();
537    if mods.logo {
538        combo.push_str("Super+");
539    }
540    if mods.ctrl {
541        combo.push_str("Ctrl+");
542    }
543    if mods.shift {
544        combo.push_str("Shift+");
545    }
546    if mods.alt {
547        combo.push_str("Alt+");
548    }
549    combo.push_str(&name);
550    Some(combo)
551}
552
553/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
554/// `F5`… `None` si xkb no le da un nombre razonable.
555fn named_key(sym: Keysym) -> Option<String> {
556    let name = xkb::keysym_get_name(sym);
557    if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
558        None
559    } else {
560        Some(name)
561    }
562}
563
564/// Despacha los callbacks de frame de un árbol de superficies: avisa a
565/// cada cliente de que puede dibujar el siguiente cuadro.
566fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
567    with_surface_tree_downward(
568        surface,
569        (),
570        |_, _, &()| TraversalAction::DoChildren(()),
571        |_surf, states, &()| {
572            for callback in states
573                .cached_state
574                .get::<SurfaceAttributes>()
575                .current()
576                .frame_callbacks
577                .drain(..)
578            {
579                callback.done(time);
580            }
581        },
582        |_, _, &()| true,
583    );
584}
585
586// ---------------------------------------------------------------------
587// Bucle principal
588// ---------------------------------------------------------------------
589
590/// Dónde pintar una ventana: su celda asignada, pero si el cliente
591/// presenta una superficie más pequeña que la celda (p. ej. un terminal
592/// que redondea su tamaño a celdas de texto enteras), se centra en el
593/// hueco sobrante en vez de dejarlo todo a un lado.
594fn render_loc(w: &ManagedWindow) -> (i32, i32) {
595    match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
596        Some(Some(size)) => {
597            let dx = ((w.size.0 - size.w) / 2).max(0);
598            let dy = ((w.size.1 - size.h) / 2).max(0);
599            (w.loc.0 + dx, w.loc.1 + dy)
600        }
601        _ => w.loc,
602    }
603}
604
605/// El tamaño en píxeles de la superficie de una ventana, si el cliente
606/// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa
607/// el backend DRM para acertar el rectángulo en el test de impacto del
608/// puntero.
609fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
610    with_renderer_surface_state(&w.surface, |s| s.surface_size())
611        .flatten()
612        .map(|s| (s.w, s.h))
613}
614
615/// El punto caliente (hotspot) de una superficie de cursor: el píxel de
616/// la imagen que debe quedar bajo la posición real del puntero. `(0, 0)`
617/// si el cliente no lo declaró.
618fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) {
619    with_states(surface, |states| {
620        states
621            .data_map
622            .get::<CursorImageSurfaceData>()
623            .map(|m| {
624                let h = m.lock().unwrap().hotspot;
625                (h.x, h.y)
626            })
627            .unwrap_or((0, 0))
628    })
629}
630
631/// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el
632/// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se
633/// conecta a este compositor. Lo usan la acción `spawn:…` del keymap, la
634/// variable `MIRADA_STARTUP` y el autoarranque.
635fn spawn_command(cmd: &str) {
636    let cmd = cmd.trim();
637    if cmd.is_empty() {
638        return;
639    }
640    match std::process::Command::new("sh").arg("-c").arg(cmd).spawn() {
641        Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
642        Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
643    }
644}
645
646/// La ruta del archivo de autoarranque del usuario,
647/// `~/.config/mirada/autostart` — junto al keymap y las reglas.
648fn autostart_path() -> Option<std::path::PathBuf> {
649    Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart")))
650}
651
652/// Lanza los programas del archivo de autoarranque: un comando por
653/// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no
654/// hace nada. Se llama una vez al arrancar, con el socket ya abierto.
655fn spawn_autostart() {
656    let Some(path) = autostart_path() else {
657        return;
658    };
659    let Ok(text) = std::fs::read_to_string(&path) else {
660        return; // no hay archivo de autoarranque
661    };
662    let mut n = 0;
663    for line in text.lines() {
664        let line = line.trim();
665        if line.is_empty() || line.starts_with('#') {
666            continue;
667        }
668        spawn_command(line);
669        n += 1;
670    }
671    if n > 0 {
672        println!("mirada-compositor · autoarranque: {n} programa(s) desde {}", path.display());
673    }
674}
675
676/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
677fn load_user_rules() -> Rules {
678    match Rules::default_path() {
679        Some(p) => Rules::load_or_default(&p),
680        None => Rules::default(),
681    }
682}
683
684/// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland —
685/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
686/// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo
687/// vivo mientras el compositor corra.
688fn announce_output(
689    dh: &DisplayHandle,
690    name: &str,
691    width: i32,
692    height: i32,
693    refresh_mhz: i32,
694) -> smithay::output::Output {
695    use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
696    let output = Output::new(
697        name.to_string(),
698        PhysicalProperties {
699            size: (0, 0).into(),
700            subpixel: Subpixel::Unknown,
701            make: "mirada".into(),
702            model: name.to_string(),
703        },
704    );
705    output.create_global::<App>(dh);
706    let mode = Mode { size: (width, height).into(), refresh: refresh_mhz };
707    output.change_current_state(
708        Some(mode),
709        Some(Transform::Normal),
710        Some(Scale::Integer(1)),
711        Some((0, 0).into()),
712    );
713    output.set_preferred(mode);
714    output
715}
716
717/// Lo que comparten los dos backends gráficos: el `Display` de Wayland,
718/// el `App` ya armado y la maquinaria de keymap y control.
719struct Setup {
720    display: Display<App>,
721    app: App,
722    keymap_path: Option<std::path::PathBuf>,
723    keymap_watch: Option<mirada_brain::KeymapWatch>,
724    ctl: Option<CtlServer>,
725}
726
727/// Arma el estado del compositor — todo lo independiente del backend
728/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
729/// (winit o DRM) registra luego su propia salida y monta su bucle.
730fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
731    let display: Display<App> = Display::new()?;
732    let dh = display.handle();
733
734    let mut seat_state = SeatState::new();
735    let seat = seat_state.new_wl_seat(&dh, "mirada");
736
737    // Anuncia el gestor de decoración: las ventanas van sin marco (ver
738    // `XdgDecorationHandler`). El `XdgDecorationState` sólo serviría para
739    // retirar el global más tarde, cosa que nunca hacemos.
740    let _ = XdgDecorationState::new::<App>(&dh);
741
742    // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
743    // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
744    let keymap_path = Keymap::default_path();
745
746    // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
747    let brain = match std::env::var("MIRADA_SOCKET") {
748        Ok(path) => {
749            println!("mirada-compositor · esperando al Cerebro en {path} …");
750            let link = BodyLink::listen(&path)?;
751            println!("mirada-compositor · Cerebro conectado.");
752            Brain::Linked(link)
753        }
754        Err(_) => {
755            println!("mirada-compositor · modo autónomo (Cerebro embebido).");
756            let keymap = match &keymap_path {
757                Some(p) => Keymap::load_or_init(p),
758                None => Keymap::default(),
759            };
760            let mut desktop = Desktop::with_keymap(keymap);
761            desktop.set_rules(load_user_rules());
762            Brain::Embedded(desktop)
763        }
764    };
765
766    let mut app = App {
767        compositor_state: CompositorState::new::<App>(&dh),
768        xdg_shell_state: XdgShellState::new::<App>(&dh),
769        shm_state: ShmState::new::<App>(&dh, Vec::new()),
770        seat_state,
771        data_device_state: DataDeviceState::new::<App>(&dh),
772        seat,
773        keyboard: None,
774        pointer: None,
775        pointer_loc: (0.0, 0.0),
776        cursor_status: CursorImageStatus::default_named(),
777        drag: None,
778        windows: Vec::new(),
779        body: BodyState::new(),
780        brain,
781        grabs: Vec::new(),
782        pending_keybind: None,
783        next_id: 1,
784        running: true,
785    };
786
787    let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?;
788    app.keyboard = Some(keyboard);
789    app.pointer = Some(app.seat.add_pointer());
790
791    // En modo embebido, el propio Desktop dicta los atajos a interceptar.
792    if let Brain::Embedded(desktop) = &app.brain {
793        let grab = desktop.grab_keys();
794        app.apply_commands(vec![grab]);
795    }
796
797    // Vigilancia del keymap para recargarlo en caliente — sólo tiene
798    // sentido con el Cerebro embebido.
799    let keymap_watch = match (&app.brain, &keymap_path) {
800        (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
801        _ => None,
802    };
803    if keymap_watch.is_some() {
804        println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
805    }
806
807    // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
808    // externo, el socket de control lo abre él.
809    let ctl = match &app.brain {
810        Brain::Embedded(_) => {
811            let path = mirada_brain::ctl::default_socket_path();
812            match CtlServer::bind(&path) {
813                Ok(s) => {
814                    println!("mirada-compositor · API de control en {}", path.display());
815                    Some(s)
816                }
817                Err(e) => {
818                    eprintln!("mirada-compositor · sin API de control: {e}");
819                    None
820                }
821            }
822        }
823        Brain::Linked(_) => None,
824    };
825
826    Ok(Setup { display, app, keymap_path, keymap_watch, ctl })
827}
828
829/// El backend `winit`: corre anidado dentro de una sesión gráfica.
830fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
831    let Setup {
832        mut display,
833        app: mut state,
834        keymap_path,
835        keymap_watch,
836        ctl,
837    } = build_app()?;
838    let keyboard = state.keyboard.clone().expect("teclado inicializado");
839
840    // El backend gráfico va primero. winit abre la ventana del compositor
841    // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
842    // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
843    // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
844    // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
845    // y se quedaría colgado para siempre.
846    let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
847        Ok(pair) => pair,
848        Err(e) => {
849            eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
850            eprintln!(
851                "   El backend `winit` necesita una sesión gráfica anfitriona\n   \
852                 (X11 o Wayland) donde dibujar la ventana del compositor.\n   \
853                 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n   \
854                 XDG_SESSION_TYPE='{}'.\n   \
855                 Lánzalo desde un escritorio gráfico, o desde un servidor X\n   \
856                 virtual (Xvfb) al que te conectes por VNC.",
857                std::env::var("DISPLAY").unwrap_or_default(),
858                std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
859                std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
860            );
861            return Err(e.into());
862        }
863    };
864
865    // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
866    // publica *después* de winit, sólo para los clientes que lancemos
867    // como procesos hijos.
868    let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
869    let socket_name = listener
870        .socket_name()
871        .and_then(|s| s.to_str())
872        .unwrap_or("wayland-?")
873        .to_string();
874    std::env::set_var("WAYLAND_DISPLAY", &socket_name);
875    println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
876    println!("   lanza un cliente:  WAYLAND_DISPLAY={socket_name} foot");
877
878    let start = Instant::now();
879    let mut clients = Vec::new();
880
881    // Salida inicial = el tamaño de la ventana winit.
882    let win_size = backend.window_size();
883    let _wl_output = announce_output(&display.handle(), "winit", win_size.w, win_size.h, 60_000);
884    {
885        let ev = state.body.add_output(0, win_size.w, win_size.h);
886        state.brain_feed(ev);
887    }
888
889    while state.running {
890        // 1 · Eventos del backend (teclado, redimensión, cierre).
891        let status = winit.dispatch_new_events(|event| match event {
892            WinitEvent::CloseRequested => state.running = false,
893            WinitEvent::Resized { size, .. } => {
894                let ev = state.body.remove_output(0);
895                state.brain_feed(ev);
896                let ev = state.body.add_output(0, size.w, size.h);
897                state.brain_feed(ev);
898            }
899            WinitEvent::Input(InputEvent::Keyboard { event }) => {
900                let code = event.key_code();
901                let key_state = event.state();
902                let pressed = key_state == KeyState::Pressed;
903                let time = start.elapsed().as_millis() as u32;
904                keyboard.clone().input::<(), _>(
905                    &mut state,
906                    code,
907                    key_state,
908                    SERIAL_COUNTER.next_serial(),
909                    time,
910                    |st, mods, handle| {
911                        if !pressed {
912                            return FilterResult::Forward;
913                        }
914                        if let Some(combo) = combo_string(mods, handle.modified_sym()) {
915                            if st.grabs.contains(&combo) {
916                                st.pending_keybind = Some(combo);
917                                return FilterResult::Intercept(());
918                            }
919                        }
920                        FilterResult::Forward
921                    },
922                );
923                if let Some(combo) = state.pending_keybind.take() {
924                    let ev = state.body.keybind(combo);
925                    state.brain_feed(ev);
926                }
927            }
928            _ => {}
929        });
930        if let PumpStatus::Exit(_) = status {
931            break;
932        }
933
934        // 2 · Comandos de un Cerebro enlazado.
935        state.brain_poll();
936
937        // 2 bis · Recarga del keymap si el archivo cambió en disco.
938        if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
939            if let Some(path) = &keymap_path {
940                match Keymap::load(path) {
941                    Ok(km) => {
942                        let cmd = if let Brain::Embedded(d) = &mut state.brain {
943                            Some(d.set_keymap(km))
944                        } else {
945                            None
946                        };
947                        if let Some(cmd) = cmd {
948                            state.apply_commands(vec![cmd]);
949                        }
950                        println!("mirada-compositor · keymap recargado.");
951                    }
952                    Err(e) => eprintln!(
953                        "mirada-compositor · keymap inválido, conservo el anterior: {e}"
954                    ),
955                }
956            }
957        }
958
959        // 2 ter · Peticiones del API de control (mirada-ctl).
960        if let Some(ctl) = &ctl {
961            while let Some(mut conn) = ctl.poll() {
962                let reply = match conn.read_request() {
963                    Ok(Some(req)) => state.serve_ctl(req),
964                    Ok(None) => continue,
965                    Err(e) => CtlReply::Error(format!("{e}")),
966                };
967                let _ = conn.reply(&reply);
968            }
969        }
970
971        // 3 · Composición de las superficies en sus rectángulos.
972        let size = backend.window_size();
973        let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
974        {
975            let (renderer, mut framebuffer) = backend.bind().unwrap();
976            // Orden de pintado: la lista de elementos va front-to-back
977            // (índice 0 = encima), así que las flotantes —que deben
978            // quedar sobre las teseladas— se ordenan primero. `sort_by_key`
979            // es estable: dentro de cada grupo se respeta el orden de apertura.
980            let mut shown: Vec<&ManagedWindow> =
981                state.windows.iter().filter(|w| w.visible).collect();
982            shown.sort_by_key(|w| !w.floating);
983            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
984                .iter()
985                .flat_map(|w| {
986                    render_elements_from_surface_tree(
987                        renderer,
988                        &w.surface,
989                        render_loc(w),
990                        1.0,
991                        1.0,
992                        Kind::Unspecified,
993                    )
994                })
995                .collect();
996            let mut frame = renderer
997                .render(&mut framebuffer, size, Transform::Flipped180)
998                .unwrap();
999            frame
1000                .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
1001                .unwrap();
1002            draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
1003            let _ = frame.finish().unwrap();
1004        }
1005
1006        // 4 · Callbacks de frame + clientes nuevos + flush.
1007        let time = start.elapsed().as_millis() as u32;
1008        for w in &state.windows {
1009            send_frames_surface_tree(&w.surface, time);
1010        }
1011        if let Some(stream) = listener.accept()? {
1012            let client = display
1013                .handle()
1014                .insert_client(stream, Arc::new(ClientState::default()))
1015                .unwrap();
1016            clients.push(client);
1017        }
1018        display.dispatch_clients(&mut state)?;
1019        display.flush_clients()?;
1020
1021        backend.submit(Some(&[damage])).unwrap();
1022    }
1023
1024    println!("mirada-compositor · adiós.");
1025    Ok(())
1026}
1027
1028fn main() {
1029    let arg = std::env::args().nth(1);
1030    let result = match arg.as_deref() {
1031        Some("--drm") => drm_backend::run(),
1032        Some("--winit") => run_winit(),
1033        Some(other) => {
1034            eprintln!("mirada-compositor: opción desconocida «{other}» — usa --drm o --winit");
1035            std::process::exit(2);
1036        }
1037        None => {
1038            // Auto: con sesión gráfica anfitriona → winit (anidado);
1039            // sin ella (una TTY pelada) → backend DRM.
1040            let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
1041                || std::env::var_os("DISPLAY").is_some();
1042            if nested {
1043                println!("mirada-compositor · sesión gráfica detectada → backend winit.");
1044                run_winit()
1045            } else {
1046                println!("mirada-compositor · sin sesión gráfica → backend DRM.");
1047                drm_backend::run()
1048            }
1049        }
1050    };
1051    if let Err(e) = result {
1052        eprintln!("mirada-compositor · error: {e}");
1053        std::process::exit(1);
1054    }
1055}