gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

⎇ af3be482a948797a0e88036f3dbd87a1de44d6a1 crates/apps/mirada-compositor/src/main.rs Rust · 1183 siq'ikuna · 43.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

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