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>
This commit is contained in:
sergio
2026-05-20 22:44:39 +00:00
parent f2455b0eca
commit d2e0cf4830
6 changed files with 724 additions and 12 deletions
+19
View File
@@ -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"
+61
View File
@@ -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
+604
View File
@@ -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);
}
}
+21 -12
View File
@@ -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:
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`.
| 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` |
**Pendiente** — refinamientos del Cuerpo, no verificables en modo
desatendido:
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.
| 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.