gioser

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176798 Sutichaykuna

feat(mirada): mirada-compositor — el Cuerpo, compositor Wayland sobre smithay

Compositor Wayland teselante real sobre smithay, backend winit (corre
anidado como ventana dentro de la sesión X11/Wayland actual). Habla
wl_compositor/xdg_shell/wl_shm/wl_seat/wl_data_device y compone las
superficies de los clientes con GlesRenderer.

Dos modos: autónomo (Cerebro Desktop embebido, un solo proceso) o
enlazado (MIRADA_SOCKET → la app mirada decide la geometría). Reusa
mirada-body para la contabilidad y mirada-link para el cable.

Actualiza el SDD: el Cuerpo deja de ser pendiente. Añade README.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

d2e0cf48303a222e45a2a22723de78666832d1ca · ruwaq sergio · (5 killa ñawpaq) · tayta f2455b0ec · kay commitpi sach'ata qhaway

Semántico sayk'uy · 36

crates/apps/mirada-compositor/src/main.rs

6 qillqakuna · +726 −14

diff --git a/Cargo.toml b/Cargo.toml
index 7a5b8f4..a40aef7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -286,6 +286,7 @@ members = [
     "crates/apps/matilda",
     "crates/apps/yachay",
     "crates/apps/mirada",
+    "crates/apps/mirada-compositor",
 ]
 
 [workspace.package]
diff --git a/crates/apps/mirada-compositor/Cargo.toml b/crates/apps/mirada-compositor/Cargo.toml
new file mode 100644
index 0000000..e7547a0
--- /dev/null
+++ b/crates/apps/mirada-compositor/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "mirada-compositor"
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "mirada — el Cuerpo del compositor: un compositor Wayland teselante sobre smithay (backend winit, nested). Tesela con un Cerebro embebido o uno externo por mirada-link."
+
+[[bin]]
+name = "mirada-compositor"
+path = "src/main.rs"
+
+[dependencies]
+mirada-brain = { path = "../../modules/mirada/mirada-brain" }
+mirada-body = { path = "../../modules/mirada/mirada-body" }
+mirada-link = { path = "../../modules/mirada/mirada-link" }
+smithay = "0.7"
diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md
new file mode 100644
index 0000000..c49d6ef
--- /dev/null
+++ b/crates/apps/mirada-compositor/README.md
@@ -0,0 +1,61 @@
+# mirada-compositor — el Cuerpo de carmen
+
+Un compositor Wayland teselante real, sobre [`smithay`]. Es el **Cuerpo**
+de la arquitectura Cerebro↔Cuerpo de `mirada` (ver
+`crates/modules/mirada/SDD.md`): habla el protocolo Wayland con los
+clientes, compone sus superficies y aplica la geometría que decide el
+Cerebro.
+
+Backend `winit`: corre **anidado** — una ventana dentro de tu sesión
+gráfica actual, X11 o Wayland. No toca DRM/KMS, así que es seguro de
+arrancar sin dejar la sesión.
+
+## Dos modos
+
+- **Autónomo** (por defecto) — lleva un `Desktop` (de `mirada-brain`)
+  embebido. Es un compositor teselante completo en un solo proceso.
+
+  ```sh
+  cargo run -p mirada-compositor
+  ```
+
+- **Enlazado** — el Cuerpo escucha en un socket y la app `mirada` (el
+  Cerebro GPUI) se conecta y decide la geometría.
+
+  ```sh
+  # terminal 1 — el Cuerpo
+  MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada-compositor
+  # terminal 2 — el Cerebro
+  MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada
+  ```
+
+## Probarlo
+
+Al arrancar imprime el `WAYLAND_DISPLAY` que abrió. Lanza cualquier
+cliente Wayland contra él:
+
+```sh
+WAYLAND_DISPLAY=wayland-1 foot      # o weston-terminal, alacritty, …
+```
+
+Las ventanas se teselan solas. El teclado, con la ventana del compositor
+enfocada, maneja el escritorio con atajos `Super+…` (los que registra el
+Cerebro: foco `Super+j/k`, layout `Super+Tab`, escritorios `Super+1..9`).
+Cierra la ventana del compositor para salir.
+
+## Qué implementa
+
+`wl_compositor`, `xdg_shell` (toplevels y popups), `wl_shm`, `wl_seat`
+(teclado) y `wl_data_device` (selección). Composición con `GlesRenderer`.
+
+Reusa `mirada-body` para la contabilidad de salidas y superficies, y
+`mirada-link` para el cable hacia un Cerebro externo. Toda la lógica
+espacial es agnóstica de Wayland y vive en los crates de
+`crates/modules/mirada/`.
+
+## Pendiente
+
+Backend nativo DRM/libinput (de ventana anidada a sesión real),
+puntero/ratón completo y aislamiento de clientes. Ver el SDD.
+
+[`smithay`]: https://github.com/Smithay/smithay
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
new file mode 100644
index 0000000..67094cd
--- /dev/null
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -0,0 +1,604 @@
+//! `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::input::{InputEvent, KeyState, KeyboardKeyEvent};
+use smithay::backend::renderer::element::surface::{
+    render_elements_from_surface_tree, WaylandSurfaceRenderElement,
+};
+use smithay::backend::renderer::element::Kind;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::backend::renderer::utils::{draw_render_elements, on_commit_buffer_handler};
+use smithay::backend::renderer::{Color32F, Frame, Renderer};
+use smithay::backend::winit::{self, WinitEvent};
+use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
+use smithay::input::{Seat, SeatHandler, SeatState};
+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_seat;
+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
+use smithay::reexports::wayland_server::{Client, Display, 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::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::{
+    PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
+    XdgToplevelSurfaceData,
+};
+use smithay::wayland::shm::{ShmHandler, ShmState};
+use smithay::{
+    delegate_compositor, delegate_data_device, delegate_seat, delegate_shm, delegate_xdg_shell,
+};
+
+use mirada_body::{BodyOp, BodyState};
+use mirada_brain::{BodyEvent, BrainCommand, Desktop};
+use mirada_link::BodyLink;
+
+// ---------------------------------------------------------------------
+// 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),
+}
+
+/// Una ventana de cliente que el compositor gestiona.
+struct ManagedWindow {
+    id: u64,
+    toplevel: ToplevelSurface,
+    surface: WlSurface,
+    /// Esquina superior-izquierda en píxeles, según el Cerebro.
+    loc: (i32, i32),
+    visible: bool,
+}
+
+/// El estado global del compositor.
+struct App {
+    compositor_state: CompositorState,
+    xdg_shell_state: XdgShellState,
+    shm_state: ShmState,
+    seat_state: SeatState<Self>,
+    data_device_state: DataDeviceState,
+    seat: Seat<Self>,
+    keyboard: Option<KeyboardHandle<Self>>,
+
+    /// Ventanas gestionadas, en orden de aparición.
+    windows: Vec<ManagedWindow>,
+    /// La contabilidad del Cuerpo (mirada-body).
+    body: BodyState,
+    /// El Cerebro: embebido o enlazado.
+    brain: Brain,
+    /// Atajos globales a interceptar (los registra el Cerebro).
+    grabs: Vec<String>,
+    /// Atajo capturado en el último evento de teclado, pendiente de enviar.
+    pending_keybind: Option<String>,
+    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);
+        }
+    }
+
+    /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
+    fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
+        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 } => {
+                if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
+                    w.loc = (rect.x, rect.y);
+                    w.visible = visible;
+                    w.toplevel.with_pending_state(|s| {
+                        s.size = Some((rect.w.max(1), rect.h.max(1)).into());
+                    });
+                    w.toplevel.send_pending_configure();
+                }
+            }
+            BodyOp::Focus(id) => {
+                let mut target = None;
+                for w in &self.windows {
+                    let active = w.id == id;
+                    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 => {
+                if let Some(kb) = self.keyboard.clone() {
+                    kb.set_focus(self, Option::<WlSurface>::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::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::<XdgToplevelSurfaceData>()
+                .and_then(|d| d.lock().ok())
+                .map(|d| {
+                    (
+                        d.app_id.clone().unwrap_or_default(),
+                        d.title.clone().unwrap_or_default(),
+                    )
+                })
+                .unwrap_or_default()
+        });
+        let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
+        let title = if title.is_empty() { format!("ventana {id}") } else { title };
+
+        self.windows.push(ManagedWindow {
+            id,
+            toplevel,
+            surface,
+            loc: (0, 0),
+            visible: false,
+        });
+        let ev = self.body.open_surface(id, app_id, title);
+        self.brain_feed(ev);
+    }
+}
+
+// ---------------------------------------------------------------------
+// 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::<ClientState>().unwrap().compositor_state
+    }
+
+    fn commit(&mut self, surface: &WlSurface) {
+        on_commit_buffer_handler::<Self>(surface);
+    }
+}
+
+impl BufferHandler for App {
+    fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
+}
+
+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 id = self.windows.remove(pos).id;
+            if let Some(ev) = self.body.close_surface(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::<XdgToplevelSurfaceData>()
+                .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 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,
+    ) {
+    }
+}
+
+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<Self>) {}
+}
+
+impl SeatHandler for App {
+    type KeyboardFocus = WlSurface;
+    type PointerFocus = WlSurface;
+    type TouchFocus = WlSurface;
+
+    fn seat_state(&mut self) -> &mut SeatState<Self> {
+        &mut self.seat_state
+    }
+
+    fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
+    fn cursor_image(
+        &mut self,
+        _seat: &Seat<Self>,
+        _image: smithay::input::pointer::CursorImageStatus,
+    ) {
+    }
+}
+
+delegate_compositor!(App);
+delegate_xdg_shell!(App);
+delegate_shm!(App);
+delegate_seat!(App);
+delegate_data_device!(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<String> {
+    let utf = xkb::keysym_to_utf8(sym);
+    let key = utf.trim_end_matches('\0');
+    let name = if key == " " {
+        "space".to_string()
+    } else {
+        let mut chars = key.chars();
+        let c = chars.next()?;
+        if chars.next().is_some() || !c.is_ascii_graphic() {
+            return None;
+        }
+        c.to_ascii_lowercase().to_string()
+    };
+    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)
+}
+
+/// 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::<SurfaceAttributes>()
+                .current()
+                .frame_callbacks
+                .drain(..)
+            {
+                callback.done(time);
+            }
+        },
+        |_, _, &()| true,
+    );
+}
+
+// ---------------------------------------------------------------------
+// Bucle principal
+// ---------------------------------------------------------------------
+
+fn run() -> Result<(), Box<dyn std::error::Error>> {
+    let mut display: Display<App> = Display::new()?;
+    let dh = display.handle();
+
+    let mut seat_state = SeatState::new();
+    let seat = seat_state.new_wl_seat(&dh, "mirada");
+
+    // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
+    let brain = 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).");
+            Brain::Embedded(Desktop::new())
+        }
+    };
+
+    let mut state = App {
+        compositor_state: CompositorState::new::<App>(&dh),
+        xdg_shell_state: XdgShellState::new::<App>(&dh),
+        shm_state: ShmState::new::<App>(&dh, Vec::new()),
+        seat_state,
+        data_device_state: DataDeviceState::new::<App>(&dh),
+        seat,
+        keyboard: None,
+        windows: Vec::new(),
+        body: BodyState::new(),
+        brain,
+        grabs: Vec::new(),
+        pending_keybind: None,
+        next_id: 1,
+        running: true,
+    };
+
+    let keyboard = state.seat.add_keyboard(Default::default(), 200, 25)?;
+    state.keyboard = Some(keyboard.clone());
+
+    // En modo embebido, el propio Desktop dicta los atajos a interceptar.
+    if let Brain::Embedded(desktop) = &state.brain {
+        let grab = desktop.grab_keys();
+        state.apply_commands(vec![grab]);
+    }
+
+    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 (mut backend, mut winit) = winit::init::<GlesRenderer>()?;
+    let start = Instant::now();
+    let mut clients = Vec::new();
+
+    // Salida inicial = el tamaño de la ventana winit.
+    {
+        let size = backend.window_size();
+        let ev = state.body.add_output(0, size.w, size.h);
+        state.brain_feed(ev);
+    }
+
+    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, .. } => {
+                let ev = state.body.remove_output(0);
+                state.brain_feed(ev);
+                let ev = state.body.add_output(0, size.w, size.h);
+                state.brain_feed(ev);
+            }
+            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();
+
+        // 3 · Composición de las superficies en sus rectángulos.
+        let size = backend.window_size();
+        let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
+        {
+            let (renderer, mut framebuffer) = backend.bind().unwrap();
+            let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = state
+                .windows
+                .iter()
+                .filter(|w| w.visible)
+                .flat_map(|w| {
+                    render_elements_from_surface_tree(
+                        renderer,
+                        &w.surface,
+                        w.loc,
+                        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() {
+    if let Err(e) = run() {
+        eprintln!("mirada-compositor · error: {e}");
+        std::process::exit(1);
+    }
+}
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 96be630..08d2534 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -26,6 +26,7 @@ ejecuta operaciones de geometría".
 | `mirada-link`      | lib      | Transporte: el socket Unix con hilo lector + canal                     |
 | `mirada-body`      | lib      | Contabilidad del Cuerpo: `BodyState`, traduce comandos a `BodyOp`      |
 | `mirada` (app)     | bin/GPUI | El Cerebro: ventana que tesela el escritorio y manda geometría         |
+| `mirada-compositor`| bin/smithay | El Cuerpo: compositor Wayland real (backend `winit`, anidado)        |
 
 ## Flujo
 
@@ -72,8 +73,7 @@ ejecuta operaciones de geometría".
 
 - Todos los `lib` con `#![forbid(unsafe_code)]`. Cero Wayland, cero
   `smithay` en los seis crates de arriba.
-- El acoplamiento a hardware vive sólo en `mirada-compositor`
-  (pendiente).
+- El acoplamiento a Wayland/hardware vive sólo en `mirada-compositor`.
 
 ## Estado
 
@@ -81,15 +81,24 @@ Implementado y verde: `mirada-layout` (22 tests), `mirada-protocol`
 (9), `mirada-brain` (17), `mirada-link` (7), `mirada-body` (13), y la
 app `mirada` (compila; verificación visual manual).
 
-**Pendiente** — la capa que toca hardware/protocolo, no verificable en
-modo desatendido:
-
-| crate pendiente     | rol                                                       |
-| ------------------- | --------------------------------------------------------- |
-| `mirada-compositor` | el Cuerpo: `smithay` — superficies, buffers, salidas, DRM |
-| `mirada-input`      | libinput → teclado/ratón, intercepción de atajos          |
-| `mirada-sandbox`    | aislamiento de clientes sobre `arje-incarnate`            |
-
-El Cuerpo reusará `mirada-body` para su contabilidad y `mirada-link`
-para el cable; sólo le falta el *backend* `smithay`. CRIU
-(congelar/restaurar ventanas) queda anotado como futuro.
+El **Cuerpo** ya existe: `mirada-compositor` es un compositor Wayland
+teselante real sobre `smithay`, con backend `winit` — corre **anidado**
+como una ventana dentro de la sesión gráfica actual. Habla
+`wl_compositor`/`xdg_shell`/`wl_shm`/`wl_seat`/`wl_data_device`, compone
+las superficies de los clientes con `GlesRenderer` y aplica la geometría
+del Cerebro. Reusa `mirada-body` (contabilidad) y `mirada-link` (cable).
+Dos modos: **autónomo** (Cerebro `Desktop` embebido, un solo proceso) o
+**enlazado** (`MIRADA_SOCKET` → la app `mirada` decide la geometría).
+Compila y pasa clippy; verificación visual manual — ver
+`crates/apps/mirada-compositor/README.md`.
+
+**Pendiente** — refinamientos del Cuerpo, no verificables en modo
+desatendido:
+
+| capa pendiente       | rol                                                      |
+| -------------------- | -------------------------------------------------------- |
+| backend DRM/libinput | de `winit` anidado a sesión nativa: superficies KMS, GPU |
+| `mirada-input`       | puntero/ratón completo, repetición de teclas, gestos     |
+| `mirada-sandbox`     | aislamiento de clientes sobre `arje-incarnate`           |
+
+CRIU (congelar/restaurar ventanas) queda anotado como futuro.
diff --git a/vamos.txt b/vamos.txt
index 1e31f34..096862e 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -985,4 +985,22 @@
 
 
 
+  Flujo end-to-end demoable hoy sin gráficos:
+  cargo run -p mirada-body --example headless -- /tmp/mirada.sock   # el Cuerpo
+  MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada                # el Cerebro
+  Sin MIRADA_SOCKET, mirada corre en simulación: ventanas sintéticas y teclado propio (n abre, j/k foco, tab cicla layout, 1-9 escritorios).
+
+
+
+  El Cuerpo de verdad — mirada-compositor:
+  Compositor Wayland teselante real sobre smithay, backend winit (corre anidado, una ventana dentro de tu sesión X11/Wayland).
+  cargo run -p mirada-compositor                                   # autónomo: Cerebro Desktop embebido, un solo proceso
+  WAYLAND_DISPLAY=wayland-1 foot                                    # lanza clientes contra él (imprime su WAYLAND_DISPLAY al arrancar)
+  MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada-compositor     # enlazado: la app mirada (Cerebro GPUI) decide la geometría
+  Habla wl_compositor/xdg_shell/wl_shm/wl_seat/wl_data_device; compone con GlesRenderer. Reusa mirada-body y mirada-link.
+  Ver crates/apps/mirada-compositor/README.md.
+
+
+
+