//! `mirada-compositor` — el Cuerpo del compositor carmen. //! //! Un compositor Wayland teselante real, sobre `smithay`, con backend //! `winit`: corre **anidado** como una ventana dentro de tu sesión //! gráfica actual (X11 o Wayland). Habla el protocolo Wayland con los //! clientes, compone sus superficies y aplica la geometría que decide el //! Cerebro. //! //! Dos modos: //! //! - **Autónomo** (por defecto): lleva un [`Desktop`] embebido — es un //! compositor teselante completo en un solo proceso. Lánzalo y abre //! clientes; el teclado (`Super+…`) maneja el escritorio. //! - **Enlazado** (`MIRADA_SOCKET=/ruta`): el Cuerpo escucha ahí y la //! app `mirada` (el Cerebro GPUI) se conecta; la geometría viaja por //! [`mirada_link`]. //! //! Cómo probarlo en un Linux real: ver `crates/apps/mirada-compositor/README.md`. use std::sync::Arc; use std::time::Instant; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent}; use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, }; use smithay::backend::renderer::element::solid::SolidColorBuffer; use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::utils::{ draw_render_elements, on_commit_buffer_handler, with_renderer_surface_state, }; use smithay::backend::renderer::{Color32F, Frame, ImportDma, Renderer}; use smithay::backend::winit::{self, WinitEvent}; use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState}; use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData, PointerHandle}; use smithay::input::{Seat, SeatHandler, SeatState}; use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason}; use smithay::reexports::wayland_server::protocol::wl_buffer; use smithay::reexports::wayland_server::protocol::wl_output; use smithay::reexports::wayland_server::protocol::wl_seat; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket}; use smithay::reexports::winit::platform::pump_events::PumpStatus; use smithay::utils::{Rectangle, SERIAL_COUNTER}; use smithay::utils::{Serial, Transform}; use smithay::wayland::buffer::BufferHandler; use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; use smithay::wayland::compositor::{ with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, TraversalAction, }; use smithay::wayland::selection::data_device::{ ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, }; use smithay::wayland::selection::SelectionHandler; use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState}; use smithay::wayland::shell::xdg::{ PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData, }; use smithay::wayland::output::OutputHandler; use smithay::wayland::shm::{ShmHandler, ShmState}; use smithay::{ delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat, delegate_shm, delegate_xdg_decoration, delegate_xdg_shell, }; use brahman_auth::{SessionTicket, UserInfo}; use mirada_body::{BodyOp, BodyState}; use mirada_brain::{ BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules, }; use mirada_link::BodyLink; mod drm_backend; // --------------------------------------------------------------------- // Estado // --------------------------------------------------------------------- /// De dónde salen las decisiones de geometría. enum Brain { /// El compositor lleva su propio `Desktop` — proceso único. Embedded(Desktop), /// Un Cerebro externo (la app `mirada`) por socket. Linked(BodyLink), } /// La fase del ciclo de vida del Cuerpo. Es un eje **ortogonal** a /// [`Brain`]: `Brain` dice de dónde sale la geometría; `BodyMode` dice /// si el compositor está pidiendo credenciales o sirviendo una sesión. /// Un arranque normal nace ya en [`BodyMode::Session`]; un arranque de /// DM (`--greeter`) nace en [`BodyMode::Greeter`] y muta una sola vez, /// al recibir el tiquet de un login válido — la «mutación atómica». #[derive(Clone, Copy, PartialEq, Eq)] enum BodyMode { /// Pantalla de login: el único cliente es el greeter, no se /// registran atajos, se rechaza `Spawn` y no hay autoarranque. Greeter, /// Sesión de usuario: el compositor funciona con normalidad. Session, } /// `app_id` que distingue a la ventana del shell del escritorio. carmen /// no la tesela: la acopla a una franja al pie de la pantalla. const SHELL_APP_ID: &str = "carmen.shell"; /// Alto en píxeles de la franja del shell, al pie de la salida. const SHELL_DOCK_HEIGHT: i32 = 40; /// Una ventana de cliente que el compositor gestiona. struct ManagedWindow { id: u64, toplevel: ToplevelSurface, surface: WlSurface, /// Esquina superior-izquierda de la celda asignada, según el Cerebro. loc: (i32, i32), /// Tamaño de la celda asignada — para centrar la ventana si el /// cliente presenta una superficie más pequeña. size: (i32, i32), visible: bool, /// `true` si flota: se compone por encima de las teseladas. floating: bool, /// `true` si tiene el foco del teclado — pinta el marco resaltado. focused: bool, /// `true` si es la ventana del shell — acoplada al pie, sin teselar. is_shell: bool, /// Búferes de los 4 lados del marco (arriba, abajo, izq., der.) — /// cada uno con su `Id` estable para el seguimiento de daño. borders: [SolidColorBuffer; 4], } /// Un arrastre de ratón en curso: mueve o redimensiona una ventana. struct DragGrab { /// La ventana que se arrastra. id: u64, /// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho). mode: DragMode, /// Posición del puntero al empezar el arrastre. start_pointer: (f64, f64), /// Rectángulo `(x, y, w, h)` de la ventana al empezar. start_rect: (i32, i32, i32, i32), } /// Qué le hace un arrastre a la ventana. #[derive(Clone, Copy)] enum DragMode { /// Reubicar la ventana — la esquina la sigue al puntero. Move, /// Redimensionarla — la esquina inferior-derecha sigue al puntero. Resize, } /// El estado global del compositor. struct App { compositor_state: CompositorState, xdg_shell_state: XdgShellState, shm_state: ShmState, /// Estado de `zwp_linux_dmabuf` — deja que los clientes con GPU /// (apps GPUI, navegadores acelerados) compartan búferes de vídeo. dmabuf_state: DmabufState, seat_state: SeatState, data_device_state: DataDeviceState, seat: Seat, keyboard: Option>, pointer: Option>, /// Posición del puntero en coordenadas globales. pointer_loc: (f64, f64), /// Qué cursor pide el cliente enfocado — una superficie suya, un /// cursor con nombre, u oculto. El backend lo pinta en consecuencia. cursor_status: CursorImageStatus, /// Arrastre de ventana en curso (mover o redimensionar con el ratón). drag: Option, /// Tamaño real de la salida (con la franja del shell incluida) — lo /// fija el backend; sirve para acoplar la ventana del shell. output_size: (i32, i32), /// Ventanas gestionadas, en orden de aparición. windows: Vec, /// La contabilidad del Cuerpo (mirada-body). body: BodyState, /// El Cerebro: embebido o enlazado. brain: Brain, /// Fase del ciclo de vida — login o sesión (ver [`BodyMode`]). mode: BodyMode, /// Identidad a la que rebajar privilegios al lanzar procesos de /// sesión. `None` salvo tras el traspaso del DM — entonces cada /// `spawn` hace `setuid`/`setgid` a este usuario (si somos root). session_user: Option, /// Atajos globales a interceptar (los registra el Cerebro). grabs: Vec, /// Atajo capturado en el último evento de teclado, pendiente de enviar. pending_keybind: Option, next_id: u64, running: bool, } impl App { /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta. fn brain_feed(&mut self, event: BodyEvent) { let cmds = match &mut self.brain { Brain::Embedded(desktop) => desktop.on_event(event), Brain::Linked(link) => { let _ = link.send(&event); Vec::new() } }; self.apply_commands(cmds); } /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido). fn brain_poll(&mut self) { let cmds = match &self.brain { Brain::Linked(link) => link.drain(), Brain::Embedded(_) => Vec::new(), }; if !cmds.is_empty() { self.apply_commands(cmds); } } /// Atiende una petición del API de control (`mirada-ctl`). fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply { match req { CtlRequest::Do(action) => { let cmds = match &mut self.brain { Brain::Embedded(d) => Some(d.apply(action)), Brain::Linked(_) => None, }; match cmds { Some(cmds) => { self.apply_commands(cmds); CtlReply::Ok } None => CtlReply::Error( "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(), ), } } CtlRequest::ListWindows => match &self.brain { Brain::Embedded(d) => CtlReply::Windows(d.window_lines()), Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()), }, } } /// Traduce los comandos del Cerebro a operaciones y las ejecuta. fn apply_commands(&mut self, cmds: Vec) { for cmd in cmds { let ops = self.body.apply(cmd); for op in ops { self.exec_op(op); } } } /// Ejecuta una operación concreta sobre las superficies reales. fn exec_op(&mut self, op: BodyOp) { match op { BodyOp::Configure { id, rect, visible, floating, fullscreen } => { if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) { w.loc = (rect.x, rect.y); w.size = (rect.w, rect.h); w.visible = visible; w.floating = floating; w.toplevel.with_pending_state(|s| { s.size = Some((rect.w.max(1), rect.h.max(1)).into()); if fullscreen { s.states.set(xdg_toplevel::State::Fullscreen); } else { s.states.unset(xdg_toplevel::State::Fullscreen); } }); w.toplevel.send_pending_configure(); } } BodyOp::Focus(id) => { let mut target = None; for w in &mut self.windows { let active = w.id == id; w.focused = active; if active { target = Some(w.surface.clone()); } w.toplevel.with_pending_state(|s| { if active { s.states.set(xdg_toplevel::State::Activated); } else { s.states.unset(xdg_toplevel::State::Activated); } }); w.toplevel.send_pending_configure(); } if let Some(kb) = self.keyboard.clone() { kb.set_focus(self, target, SERIAL_COUNTER.next_serial()); } } BodyOp::Unfocus => { for w in &mut self.windows { w.focused = false; } if let Some(kb) = self.keyboard.clone() { kb.set_focus(self, Option::::None, SERIAL_COUNTER.next_serial()); } } BodyOp::CloseClient(id) | BodyOp::KillClient(id) => { if let Some(w) = self.windows.iter().find(|w| w.id == id) { w.toplevel.send_close(); } } BodyOp::SetGrabs(keys) => self.grabs = keys, BodyOp::SetCursor(_) => {} BodyOp::Spawn(cmd) => { // En modo greeter no se lanza nada: la pantalla de login // no es un sitio desde donde abrir programas. if self.mode == BodyMode::Greeter { eprintln!("mirada-compositor · «{cmd}» rechazado — modo greeter."); } else { spawn_command(&cmd, self.session_user.as_ref()); } } BodyOp::Shutdown => self.running = false, } } /// Registra un toplevel recién creado y avisa al Cerebro. fn register_toplevel(&mut self, toplevel: ToplevelSurface) { let surface = toplevel.wl_surface().clone(); let id = self.next_id; self.next_id += 1; let (app_id, title) = with_states(&surface, |states| { states .data_map .get::() .and_then(|d| d.lock().ok()) .map(|d| { ( d.app_id.clone().unwrap_or_default(), d.title.clone().unwrap_or_default(), ) }) .unwrap_or_default() }); // La ventana del shell no se tesela: carmen la acopla al pie. let is_shell = app_id == SHELL_APP_ID; self.windows.push(ManagedWindow { id, toplevel, surface, loc: (0, 0), size: (0, 0), visible: false, floating: false, focused: false, is_shell, borders: std::array::from_fn(|_| SolidColorBuffer::default()), }); if is_shell { self.dock_shell(); } else { let app_id = if app_id.is_empty() { "cliente".into() } else { app_id }; let title = if title.is_empty() { format!("ventana {id}") } else { title }; let ev = self.body.open_surface(id, app_id, title); self.brain_feed(ev); } } /// Acopla la ventana del shell: le reserva una franja al pie de la /// salida —el Cerebro tesela el área que queda— y la dimensiona y /// coloca ahí. Se llama al registrarla y al cambiar el tamaño de la /// salida. fn dock_shell(&mut self) { let (ow, oh) = self.output_size; if ow == 0 || oh == 0 { return; // la salida todavía no está lista } // Reserva la franja: el Cerebro tesela en el alto que queda. let ev = self.body.resize_output(0, ow, oh - SHELL_DOCK_HEIGHT); self.brain_feed(ev); // Dimensiona la ventana del shell y la fija en la franja. if let Some(w) = self.windows.iter_mut().find(|w| w.is_shell) { w.loc = (0, oh - SHELL_DOCK_HEIGHT); w.size = (ow, SHELL_DOCK_HEIGHT); w.visible = true; w.toplevel.with_pending_state(|s| { s.size = Some((ow.max(1), SHELL_DOCK_HEIGHT.max(1)).into()); }); w.toplevel.send_pending_configure(); } } /// El backend informa de un tamaño de salida nuevo (arranque o /// redimensión). Si hay shell acoplado, recoloca su franja; si no, /// le pasa el área entera al Cerebro. fn output_changed(&mut self, width: i32, height: i32) { self.output_size = (width, height); if self.windows.iter().any(|w| w.is_shell) { self.dock_shell(); } else { let ev = self.body.resize_output(0, width, height); self.brain_feed(ev); } } /// El traspaso del DM — la «mutación atómica». Llega el tiquet de un /// login válido y el compositor pasa de la pantalla de greeter a la /// sesión del usuario **sin reiniciar el servidor Wayland**: el mismo /// proceso, la misma GPU, las mismas ventanas. Idempotente — un /// segundo tiquet (no debería llegar) se ignora. fn complete_greeter_handoff(&mut self, ticket: SessionTicket) { if self.mode == BodyMode::Session { return; // ya en sesión — un tiquet de más, se ignora } println!( "mirada-compositor · traspaso a la sesión de «{}» (uid {}).", ticket.user.name, ticket.user.uid ); if !nix::unistd::geteuid().is_root() { eprintln!( "mirada-compositor · aviso: no corro como root — la sesión \ heredará mis privilegios, sin setuid al usuario." ); } self.mode = BodyMode::Session; self.session_user = Some(ticket.user.clone()); // Ya en sesión: registra los atajos del escritorio (en modo // greeter se omitieron a propósito — ver `build_app`). if let Brain::Embedded(desktop) = &self.brain { let grab = desktop.grab_keys(); self.apply_commands(vec![grab]); } // Arranca la sesión: el comando del tiquet, o el autoarranque // del usuario si el tiquet no trae ninguno. let user = self.session_user.clone(); if ticket.session.trim().is_empty() { spawn_autostart(user.as_ref()); } else { spawn_command(&ticket.session, user.as_ref()); } } } // --------------------------------------------------------------------- // Handlers de protocolo // --------------------------------------------------------------------- impl CompositorHandler for App { fn compositor_state(&mut self) -> &mut CompositorState { &mut self.compositor_state } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { &client.get_data::().unwrap().compositor_state } fn commit(&mut self, surface: &WlSurface) { on_commit_buffer_handler::(surface); } } impl BufferHandler for App { fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} } impl DmabufHandler for App { fn dmabuf_state(&mut self) -> &mut DmabufState { &mut self.dmabuf_state } /// Un cliente importó un DMA-BUF. El `GlesRenderer` lo importará de /// verdad al componer; aquí basta con aceptarlo — un búfer inválido /// sólo dejará en blanco ese cuadro de esa ventana. fn dmabuf_imported( &mut self, _global: &DmabufGlobal, _dmabuf: Dmabuf, notifier: ImportNotifier, ) { let _ = notifier.successful::(); } } impl ShmHandler for App { fn shm_state(&self) -> &ShmState { &self.shm_state } } impl XdgShellHandler for App { fn xdg_shell_state(&mut self) -> &mut XdgShellState { &mut self.xdg_shell_state } fn new_toplevel(&mut self, surface: ToplevelSurface) { surface.with_pending_state(|s| { s.states.set(xdg_toplevel::State::Activated); }); surface.send_configure(); self.register_toplevel(surface); } fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { let pos = self .windows .iter() .position(|w| w.surface == *surface.wl_surface()); if let Some(pos) = pos { let w = self.windows.remove(pos); if w.is_shell { // El shell se cerró: libera su franja, el Cerebro vuelve // a teselar en la salida entera. let (ow, oh) = self.output_size; if ow != 0 && oh != 0 { let ev = self.body.resize_output(0, ow, oh); self.brain_feed(ev); } } else if let Some(ev) = self.body.close_surface(w.id) { self.brain_feed(ev); } } } fn title_changed(&mut self, surface: ToplevelSurface) { let id = self .windows .iter() .find(|w| w.surface == *surface.wl_surface()) .map(|w| w.id); let Some(id) = id else { return }; let title = with_states(surface.wl_surface(), |states| { states .data_map .get::() .and_then(|d| d.lock().ok()) .and_then(|d| d.title.clone()) .unwrap_or_default() }); if let Some(ev) = self.body.retitle_surface(id, title) { self.brain_feed(ev); } } fn fullscreen_request( &mut self, surface: ToplevelSurface, _output: Option, ) { let id = self .windows .iter() .find(|w| w.surface == *surface.wl_surface()) .map(|w| w.id); if let Some(id) = id { self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true }); } } fn unfullscreen_request(&mut self, surface: ToplevelSurface) { let id = self .windows .iter() .find(|w| w.surface == *surface.wl_surface()) .map(|w| w.id); if let Some(id) = id { self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false }); } } fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { let _ = surface.send_configure(); } fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {} fn reposition_request( &mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32, ) { } } /// Decoración de ventana: carmen tesela, así que las ventanas no llevan /// barra de título. Le decimos a todo cliente que la decoración la pone /// el servidor (`ServerSide`) — y como el servidor no dibuja ninguna, la /// ventana queda sin marco. Sin esto, clientes como `foot` se dibujan su /// propia barra (CSD), que estorba en un escritorio teselante. impl XdgDecorationHandler for App { fn new_decoration(&mut self, toplevel: ToplevelSurface) { toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide)); toplevel.send_configure(); } fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) { toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide)); toplevel.send_configure(); } fn unset_mode(&mut self, toplevel: ToplevelSurface) { toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide)); toplevel.send_configure(); } } impl SelectionHandler for App { type SelectionUserData = (); } impl DataDeviceHandler for App { fn data_device_state(&self) -> &DataDeviceState { &self.data_device_state } } impl ClientDndGrabHandler for App {} impl ServerDndGrabHandler for App { fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat) {} } impl SeatHandler for App { type KeyboardFocus = WlSurface; type PointerFocus = WlSurface; type TouchFocus = WlSurface; fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) {} /// El cliente enfocado pidió un cursor — guardamos su petición; el /// backend la pinta (su superficie, o el cuadrado si es con nombre). fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { self.cursor_status = image; } } /// El protocolo `wl_output` no necesita estado propio — basta con /// anunciar el global para que los clientes vean que hay un monitor. impl OutputHandler for App {} delegate_compositor!(App); delegate_xdg_shell!(App); delegate_xdg_decoration!(App); delegate_dmabuf!(App); delegate_shm!(App); delegate_seat!(App); delegate_data_device!(App); delegate_output!(App); // --------------------------------------------------------------------- // Datos por cliente // --------------------------------------------------------------------- #[derive(Default)] struct ClientState { compositor_state: CompositorClientState, } impl ClientData for ClientState { fn initialized(&self, _id: ClientId) {} fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {} } // --------------------------------------------------------------------- // Utilidades // --------------------------------------------------------------------- /// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de /// modificadores y el keysym, con el mismo formato que el mapa de teclas /// de [`mirada_brain`]. `None` si no es una tecla mapeable. fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option { let utf = xkb::keysym_to_utf8(sym); let key = utf.trim_end_matches('\0'); let name = if key == " " { "space".to_string() } else { // ¿Es un único carácter imprimible? Entonces la tecla es ese carácter. let mut chars = key.chars(); match (chars.next(), chars.next()) { (Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(), // Si no, una tecla con nombre: Return, Tab, Up, F5… _ => named_key(sym)?, } }; let mut combo = String::new(); if mods.logo { combo.push_str("Super+"); } if mods.ctrl { combo.push_str("Ctrl+"); } if mods.shift { combo.push_str("Shift+"); } if mods.alt { combo.push_str("Alt+"); } combo.push_str(&name); Some(combo) } /// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`, /// `F5`… `None` si xkb no le da un nombre razonable. fn named_key(sym: Keysym) -> Option { let name = xkb::keysym_get_name(sym); if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") { None } else { Some(name) } } /// Despacha los callbacks de frame de un árbol de superficies: avisa a /// cada cliente de que puede dibujar el siguiente cuadro. fn send_frames_surface_tree(surface: &WlSurface, time: u32) { with_surface_tree_downward( surface, (), |_, _, &()| TraversalAction::DoChildren(()), |_surf, states, &()| { for callback in states .cached_state .get::() .current() .frame_callbacks .drain(..) { callback.done(time); } }, |_, _, &()| true, ); } // --------------------------------------------------------------------- // Bucle principal // --------------------------------------------------------------------- /// Dónde pintar una ventana. La del shell se ancla al pie de la salida /// y crece hacia arriba (su cajón de resultados se despliega sobre las /// ventanas). Una ventana normal va en su celda; si el cliente presenta /// una superficie más pequeña que la celda (p. ej. un terminal que /// redondea su tamaño a celdas de texto), se centra en el hueco. fn render_loc(w: &ManagedWindow, output_h: i32) -> (i32, i32) { if w.is_shell { let h = surface_px_size(w).map(|(_, h)| h).unwrap_or(SHELL_DOCK_HEIGHT); return (0, output_h - h); } match with_renderer_surface_state(&w.surface, |s| s.surface_size()) { Some(Some(size)) => { let dx = ((w.size.0 - size.w) / 2).max(0); let dy = ((w.size.1 - size.h) / 2).max(0); (w.loc.0 + dx, w.loc.1 + dy) } _ => w.loc, } } /// El tamaño en píxeles de la superficie de una ventana, si el cliente /// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa /// el backend DRM para acertar el rectángulo en el test de impacto del /// puntero. fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> { with_renderer_surface_state(&w.surface, |s| s.surface_size()) .flatten() .map(|s| (s.w, s.h)) } /// El punto caliente (hotspot) de una superficie de cursor: el píxel de /// la imagen que debe quedar bajo la posición real del puntero. `(0, 0)` /// si el cliente no lo declaró. fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) { with_states(surface, |states| { states .data_map .get::() .map(|m| { let h = m.lock().unwrap().hotspot; (h.x, h.y) }) .unwrap_or((0, 0)) }) } /// Variables de entorno de tema que el compositor inyecta a cada hijo, /// para uniformizar GTK y Qt: /// - `XDG_CURRENT_DESKTOP=mirada` hace que `xdg-desktop-portal` enrute /// hacia `mirada-portal` (el backend de `org.freedesktop.appearance`). /// - `QT_QPA_PLATFORMTHEME=gtk3` hace que las apps Qt sigan el tema GTK, /// y por tanto el `gtk.css` que genera `nahual-theme`. const THEME_ENV: &[(&str, &str)] = &[ ("XDG_CURRENT_DESKTOP", "mirada"), ("QT_QPA_PLATFORMTHEME", "gtk3"), ]; /// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el /// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se /// conecta a este compositor; además se le inyecta [`THEME_ENV`] para /// que GTK y Qt adopten el tema del escritorio. Lo usan la acción /// `spawn:…` del keymap, la variable `MIRADA_STARTUP` y el autoarranque. /// /// `as_user`: si viene una identidad y el compositor corre como root /// (modo DM, tras el traspaso), el hijo baja a ese usuario — ver /// [`apply_user`]. Con `None`, o sin ser root, lanza con la identidad /// actual del compositor. fn spawn_command(cmd: &str, as_user: Option<&UserInfo>) { let cmd = cmd.trim(); if cmd.is_empty() { return; } let mut command = std::process::Command::new("sh"); command.arg("-c").arg(cmd).envs(THEME_ENV.iter().copied()); if let Some(user) = as_user { if nix::unistd::geteuid().is_root() { apply_user(&mut command, user); } } match command.spawn() { Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()), Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"), } } /// Prepara un `Command` para que el hijo corra como `user`: fija grupos /// suplementarios, gid, uid y una sesión propia, hace `cd` a su home e /// inyecta las variables de identidad. Sólo se llama tras comprobar que /// el compositor es root. /// /// La lista de grupos se calcula **en el padre**: `getgrouplist` /// consulta NSS (abre `/etc/group`), y eso no es seguro entre `fork` y /// `exec`; en `pre_exec` quedan sólo syscalls async-signal-safe. fn apply_user(command: &mut std::process::Command, user: &UserInfo) { use nix::unistd::{setgid, setgroups, setuid, Gid, Uid}; use std::os::unix::process::CommandExt; let uid = Uid::from_raw(user.uid); let gid = Gid::from_raw(user.gid); let groups: Vec = std::ffi::CString::new(user.name.as_bytes()) .ok() .and_then(|name| nix::unistd::getgrouplist(&name, gid).ok()) .unwrap_or_else(|| vec![gid]); command .env("HOME", &user.home) .env("USER", &user.name) .env("LOGNAME", &user.name) .env("SHELL", &user.shell) .current_dir(&user.home); // SAFETY: corre en el hijo, entre `fork` y `exec`. Sólo syscalls // async-signal-safe. El orden es obligatorio: grupos y gid ANTES que // uid — al rebajar el uid se pierde el privilegio para fijarlos. unsafe { command.pre_exec(move || { setgroups(&groups)?; setgid(gid)?; setuid(uid)?; let _ = nix::unistd::setsid(); // sesión propia; no es crítico Ok(()) }); } } /// La ruta del archivo de autoarranque, `…/mirada/autostart` — junto al /// keymap y las reglas. Con un usuario (tras el traspaso del DM) se /// resuelve bajo su home; sin él, bajo la config del proceso actual. fn autostart_path(user: Option<&UserInfo>) -> Option { match user { Some(u) => Some(u.home.join(".config/mirada/autostart")), None => Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart"))), } } /// Lanza los programas del archivo de autoarranque: un comando por /// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no /// hace nada. Se llama una vez al arrancar (o tras el traspaso del DM), /// con el socket ya abierto. `as_user` se propaga a [`spawn_command`]. fn spawn_autostart(as_user: Option<&UserInfo>) { let Some(path) = autostart_path(as_user) else { return; }; let Ok(text) = std::fs::read_to_string(&path) else { return; // no hay archivo de autoarranque }; let mut n = 0; for line in text.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } spawn_command(line, as_user); n += 1; } if n > 0 { println!("mirada-compositor · autoarranque: {n} programa(s) desde {}", path.display()); } } /// Nombre o ruta del binario del greeter. `MIRADA_GREETER_BIN` lo /// sobreescribe — cómodo en desarrollo para apuntar a `target/…`. fn greeter_bin() -> String { std::env::var("MIRADA_GREETER_BIN").unwrap_or_else(|_| "mirada-greeter".to_string()) } /// Lanza `mirada-greeter` como proceso hijo, en modo DM, con el stdout /// capturado. Un hilo lee sus líneas: la que sea un [`SessionTicket`] se /// entrega por `send` (el bucle de eventos hará el traspaso); el resto /// del stdout se reenvía a la consola con el prefijo `greeter ·`. El /// hilo es dueño del `Child` y lo cosecha cuando el greeter termina. fn spawn_greeter(send: S) -> std::io::Result<()> where S: Fn(SessionTicket) + Send + 'static, { use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; let mut child = Command::new(greeter_bin()) .envs(THEME_ENV.iter().copied()) .stdout(Stdio::piped()) .spawn()?; let stdout = child.stdout.take().expect("stdout pedido con Stdio::piped"); println!("mirada-compositor · greeter lanzado (pid {}).", child.id()); std::thread::spawn(move || { for line in BufReader::new(stdout).lines().map_while(Result::ok) { match SessionTicket::from_line(&line) { Some(ticket) => { println!("mirada-compositor · tiquet de sesión recibido del greeter."); send(ticket); } None => println!("greeter · {line}"), } } match child.wait() { Ok(status) => println!("mirada-compositor · el greeter terminó ({status})."), Err(e) => eprintln!("mirada-compositor · wait(greeter): {e}"), } }); Ok(()) } /// Carga las reglas de ventana del usuario, o ninguna si no hay archivo. fn load_user_rules() -> Rules { match Rules::default_path() { Some(p) => Rules::load_or_default(&p), None => Rules::default(), } } /// Arma un Cerebro embebido: un `Desktop` con el keymap del usuario y /// sus reglas de ventana. Lo usan tanto el modo autónomo como el modo /// greeter (el DM es siempre autónomo — un Cerebro externo no tiene /// sentido en la pantalla de login). fn embedded_brain(keymap_path: &Option) -> Brain { let keymap = match keymap_path { Some(p) => Keymap::load_or_init(p), None => Keymap::default(), }; let mut desktop = Desktop::with_keymap(keymap); desktop.set_rules(load_user_rules()); Brain::Embedded(desktop) } /// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland — /// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno. /// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo /// vivo mientras el compositor corra. fn announce_output( dh: &DisplayHandle, name: &str, width: i32, height: i32, refresh_mhz: i32, ) -> smithay::output::Output { use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel}; let output = Output::new( name.to_string(), PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "mirada".into(), model: name.to_string(), }, ); output.create_global::(dh); let mode = Mode { size: (width, height).into(), refresh: refresh_mhz }; output.change_current_state( Some(mode), Some(Transform::Normal), Some(Scale::Integer(1)), Some((0, 0).into()), ); output.set_preferred(mode); output } /// Anuncia el global `zwp_linux_dmabuf` con los formatos que el /// `GlesRenderer` admite. Hay que llamarlo una vez creado el renderer /// (no antes: los formatos salen de él) — así las apps que pintan por /// GPU (GPUI, navegadores acelerados) pueden ser clientes del compositor. fn announce_dmabuf(app: &mut App, dh: &DisplayHandle, renderer: &GlesRenderer) { let formats: Vec<_> = renderer.dmabuf_formats().into_iter().collect(); println!( "mirada-compositor · dmabuf: {} formato(s) anunciado(s).", formats.len() ); app.dmabuf_state.create_global::(dh, formats); } /// Lo que comparten los dos backends gráficos: el `Display` de Wayland, /// el `App` ya armado y la maquinaria de keymap y control. struct Setup { display: Display, app: App, keymap_path: Option, keymap_watch: Option, ctl: Option, } /// Arma el estado del compositor — todo lo independiente del backend /// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend /// (winit o DRM) registra luego su propia salida y monta su bucle. fn build_app(greeter: bool) -> Result> { let display: Display = Display::new()?; let dh = display.handle(); let mut seat_state = SeatState::new(); let seat = seat_state.new_wl_seat(&dh, "mirada"); // Anuncia el gestor de decoración: las ventanas van sin marco (ver // `XdgDecorationHandler`). El `XdgDecorationState` sólo serviría para // retirar el global más tarde, cosa que nunca hacemos. let _ = XdgDecorationState::new::(&dh); // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo. let keymap_path = Keymap::default_path(); // Elige el Cerebro. El modo greeter (DM) fuerza Cerebro embebido; // si no, enlazado cuando `MIRADA_SOCKET` está puesto, autónomo si no. let brain = if greeter { println!("mirada-compositor · modo greeter (DM) — Cerebro embebido."); embedded_brain(&keymap_path) } else { match std::env::var("MIRADA_SOCKET") { Ok(path) => { println!("mirada-compositor · esperando al Cerebro en {path} …"); let link = BodyLink::listen(&path)?; println!("mirada-compositor · Cerebro conectado."); Brain::Linked(link) } Err(_) => { println!("mirada-compositor · modo autónomo (Cerebro embebido)."); embedded_brain(&keymap_path) } } }; let mut app = App { compositor_state: CompositorState::new::(&dh), xdg_shell_state: XdgShellState::new::(&dh), shm_state: ShmState::new::(&dh, Vec::new()), dmabuf_state: DmabufState::new(), seat_state, data_device_state: DataDeviceState::new::(&dh), seat, keyboard: None, pointer: None, pointer_loc: (0.0, 0.0), cursor_status: CursorImageStatus::default_named(), drag: None, output_size: (0, 0), windows: Vec::new(), body: BodyState::new(), brain, mode: if greeter { BodyMode::Greeter } else { BodyMode::Session }, session_user: None, grabs: Vec::new(), pending_keybind: None, next_id: 1, running: true, }; let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?; app.keyboard = Some(keyboard); app.pointer = Some(app.seat.add_pointer()); // En modo embebido, el propio Desktop dicta los atajos a // interceptar — salvo en modo greeter: en la pantalla de login // todas las teclas van al greeter (que el usuario no pueda lanzar // nada ni cerrar el compositor). Los atajos se registran luego, en // el traspaso a la sesión (`complete_greeter_handoff`). if !greeter { if let Brain::Embedded(desktop) = &app.brain { let grab = desktop.grab_keys(); app.apply_commands(vec![grab]); } } // Vigilancia del keymap para recargarlo en caliente — sólo tiene // sentido con el Cerebro embebido y fuera del modo greeter (donde // no hay atajos registrados que recargar). let keymap_watch = match (&app.brain, &keymap_path) { (Brain::Embedded(_), Some(p)) if !greeter => Keymap::watch(p).ok(), _ => None, }; if keymap_watch.is_some() { println!("mirada-compositor · vigilando el keymap (recarga en caliente)."); } // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es // externo, el socket de control lo abre él. let ctl = match &app.brain { Brain::Embedded(_) => { let path = mirada_brain::ctl::default_socket_path(); match CtlServer::bind(&path) { Ok(s) => { println!("mirada-compositor · API de control en {}", path.display()); Some(s) } Err(e) => { eprintln!("mirada-compositor · sin API de control: {e}"); None } } } Brain::Linked(_) => None, }; Ok(Setup { display, app, keymap_path, keymap_watch, ctl }) } /// El backend `winit`: corre anidado dentro de una sesión gráfica. fn run_winit(greeter: bool) -> Result<(), Box> { let Setup { mut display, app: mut state, keymap_path, keymap_watch, ctl, } = build_app(greeter)?; let keyboard = state.keyboard.clone().expect("teclado inicializado"); // El backend gráfico va primero. winit abre la ventana del compositor // dentro de tu sesión gráfica anfitriona, y para encontrarla lee // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría // anidarse en nosotros mismos —un socket que aún no atiende a nadie— // y se quedaría colgado para siempre. let (mut backend, mut winit) = match winit::init::() { Ok(pair) => pair, Err(e) => { eprintln!("mirada-compositor · no pude abrir la ventana: {e}"); eprintln!( " El backend `winit` necesita una sesión gráfica anfitriona\n \ (X11 o Wayland) donde dibujar la ventana del compositor.\n \ Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n \ XDG_SESSION_TYPE='{}'.\n \ Lánzalo desde un escritorio gráfico, o desde un servidor X\n \ virtual (Xvfb) al que te conectes por VNC.", std::env::var("DISPLAY").unwrap_or_default(), std::env::var("WAYLAND_DISPLAY").unwrap_or_default(), std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()), ); return Err(e.into()); } }; // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se // publica *después* de winit, sólo para los clientes que lancemos // como procesos hijos. let listener = ListeningSocket::bind_auto("wayland", 1..32)?; let socket_name = listener .socket_name() .and_then(|s| s.to_str()) .unwrap_or("wayland-?") .to_string(); std::env::set_var("WAYLAND_DISPLAY", &socket_name); println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}"); println!(" lanza un cliente: WAYLAND_DISPLAY={socket_name} foot"); let start = Instant::now(); let mut clients = Vec::new(); // Con el renderer ya creado, anuncia dmabuf (clientes con GPU). announce_dmabuf(&mut state, &display.handle(), backend.renderer()); // Salida inicial = el tamaño de la ventana winit. let win_size = backend.window_size(); let _wl_output = announce_output(&display.handle(), "winit", win_size.w, win_size.h, 60_000); { let ev = state.body.add_output(0, win_size.w, win_size.h); state.brain_feed(ev); state.output_size = (win_size.w, win_size.h); } // Modo greeter (DM anidado — útil para iterar la UI del login): // lanza el greeter y recibe su tiquet por un canal que el bucle sondea. let greeter_rx = if state.mode == BodyMode::Greeter { let (tx, rx) = std::sync::mpsc::channel::(); spawn_greeter(move |ticket| { let _ = tx.send(ticket); })?; Some(rx) } else { None }; while state.running { // 1 · Eventos del backend (teclado, redimensión, cierre). let status = winit.dispatch_new_events(|event| match event { WinitEvent::CloseRequested => state.running = false, WinitEvent::Resized { size, .. } => { state.output_changed(size.w, size.h); } WinitEvent::Input(InputEvent::Keyboard { event }) => { let code = event.key_code(); let key_state = event.state(); let pressed = key_state == KeyState::Pressed; let time = start.elapsed().as_millis() as u32; keyboard.clone().input::<(), _>( &mut state, code, key_state, SERIAL_COUNTER.next_serial(), time, |st, mods, handle| { if !pressed { return FilterResult::Forward; } if let Some(combo) = combo_string(mods, handle.modified_sym()) { if st.grabs.contains(&combo) { st.pending_keybind = Some(combo); return FilterResult::Intercept(()); } } FilterResult::Forward }, ); if let Some(combo) = state.pending_keybind.take() { let ev = state.body.keybind(combo); state.brain_feed(ev); } } _ => {} }); if let PumpStatus::Exit(_) = status { break; } // 2 · Comandos de un Cerebro enlazado. state.brain_poll(); // 2 bis · El tiquet del greeter (modo DM): dispara el traspaso. if let Some(rx) = &greeter_rx { while let Ok(ticket) = rx.try_recv() { state.complete_greeter_handoff(ticket); } } // 2 ter · Recarga del keymap si el archivo cambió en disco. if keymap_watch.as_ref().is_some_and(|w| w.changed()) { if let Some(path) = &keymap_path { match Keymap::load(path) { Ok(km) => { let cmd = if let Brain::Embedded(d) = &mut state.brain { Some(d.set_keymap(km)) } else { None }; if let Some(cmd) = cmd { state.apply_commands(vec![cmd]); } println!("mirada-compositor · keymap recargado."); } Err(e) => eprintln!( "mirada-compositor · keymap inválido, conservo el anterior: {e}" ), } } } // 2 quater · Peticiones del API de control (mirada-ctl). if let Some(ctl) = &ctl { while let Some(mut conn) = ctl.poll() { let reply = match conn.read_request() { Ok(Some(req)) => state.serve_ctl(req), Ok(None) => continue, Err(e) => CtlReply::Error(format!("{e}")), }; let _ = conn.reply(&reply); } } // 3 · Composición de las superficies en sus rectángulos. let size = backend.window_size(); let damage: Rectangle = Rectangle::from_size(size); { let (renderer, mut framebuffer) = backend.bind().unwrap(); // Orden de pintado: la lista de elementos va front-to-back // (índice 0 = encima): el shell primero —va sobre todo—, luego // las flotantes, luego las teseladas. `sort_by_key` es estable: // dentro de cada grupo se respeta el orden de apertura. let output_h = state.output_size.1; let mut shown: Vec<&ManagedWindow> = state.windows.iter().filter(|w| w.visible).collect(); shown.sort_by_key(|w| (!w.is_shell, !w.floating)); let elements: Vec> = shown .iter() .flat_map(|w| { render_elements_from_surface_tree( renderer, &w.surface, render_loc(w, output_h), 1.0, 1.0, Kind::Unspecified, ) }) .collect(); let mut frame = renderer .render(&mut framebuffer, size, Transform::Flipped180) .unwrap(); frame .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage]) .unwrap(); draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap(); let _ = frame.finish().unwrap(); } // 4 · Callbacks de frame + clientes nuevos + flush. let time = start.elapsed().as_millis() as u32; for w in &state.windows { send_frames_surface_tree(&w.surface, time); } if let Some(stream) = listener.accept()? { let client = display .handle() .insert_client(stream, Arc::new(ClientState::default())) .unwrap(); clients.push(client); } display.dispatch_clients(&mut state)?; display.flush_clients()?; backend.submit(Some(&[damage])).unwrap(); } println!("mirada-compositor · adiós."); Ok(()) } fn main() { // Banderas en cualquier orden: `--greeter` (modo DM) es ortogonal // al backend (`--winit` anidado · `--drm` nativo · auto si falta). let args: Vec = std::env::args().skip(1).collect(); for a in &args { if !matches!(a.as_str(), "--greeter" | "--winit" | "--drm") { eprintln!( "mirada-compositor: opción desconocida «{a}» — usa --greeter, --winit o --drm" ); std::process::exit(2); } } let greeter = args.iter().any(|a| a == "--greeter"); let backend = args.iter().find(|a| matches!(a.as_str(), "--winit" | "--drm")); let result = match backend.map(String::as_str) { Some("--drm") => drm_backend::run(greeter), Some("--winit") => run_winit(greeter), _ => { // Auto: con sesión gráfica anfitriona → winit (anidado); // sin ella (una TTY pelada) → backend DRM. let nested = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some(); if nested { println!("mirada-compositor · sesión gráfica detectada → backend winit."); run_winit(greeter) } else { println!("mirada-compositor · sin sesión gráfica → backend DRM."); drm_backend::run(greeter) } } }; if let Err(e) = result { eprintln!("mirada-compositor · error: {e}"); std::process::exit(1); } }