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 · 2026-05-20T22:44:39Z (5 killa ñawpaq) · tayta f2455b0ec · kay commitpi sach'ata qhaway
Semántico sayk'uy · 36
crates/apps/mirada-compositor/src/main.rs
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.
+
+
+
+