Files
brahman/crates/modules/mirada/SDD.md
T
sergio ee27108f6c feat(mirada): acople del shell — ventana-dock al pie de la pantalla
Fase 2 del plan «shell»: carmen reconoce la ventana del shell y le
reserva su sitio, en vez de teselarla como una más.

Una ventana cuyo `app_id` es `carmen.shell` no entra en el teselado:
carmen le reserva una franja de 40 px al pie de la salida, la dimensiona
y la fija ahí, y la compone sobre todas las demás. El Cerebro tesela el
resto de ventanas en el área que queda.

- `mirada-protocol`: nuevo `BodyEvent::OutputResized { id, w, h }` — el
  Cerebro cambia el área útil de una salida **sin** perder el escritorio
  que muestra (a diferencia de quitar y volver a añadir la salida — que,
  de paso, era un bug latente al redimensionar la ventana winit).
- `mirada-brain`: `Desktop` atiende `OutputResized` (test nuevo).
- `mirada-body`: `BodyState::resize_output`.
- `mirada-compositor`: `ManagedWindow.is_shell`, `App.output_size`,
  `dock_shell`/`output_changed`; `register_toplevel` no registra el
  shell en el Cerebro; al cerrarse libera la franja. El shell se compone
  y se enfoca con el ratón aunque no viva en el Cerebro; no lleva marco.
  El backend winit usa ahora `resize_output` al redimensionar.

GPUI no habla `wlr-layer-shell`, así que el acople es por `app_id`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 05:38:12 +00:00

14 KiB

modules/mirada/ — Compositor Wayland (carmen)

Propósito. Un compositor Wayland teselante. La decisión de diseño central es partirlo en dos procesos, el Cerebro y el Cuerpo:

  • El Cuerpo (mirada-compositor, sobre smithay) habla Wayland con los clientes, posee el hardware (DRM/GPU/libinput) y compone las superficies reales. Los píxeles nunca salen de él — composición zero-copy.
  • El Cerebro (la app mirada, GPUI) decide dónde va cada ventana —pura aritmética de rectángulos— y orquesta el escritorio: layouts, atajos, focos, escritorios virtuales.

Los dos hablan por un socket Unix con un contrato mínimo de enums. Así, toda la lógica espacial es agnóstica de Wayland y se prueba sin un servidor gráfico; el Cuerpo queda reducido a "habla el protocolo y ejecuta operaciones de geometría".

Crates

crate tipo rol
mirada-layout lib Motor de teselado: Rect, modos de layout, Workspace (ventanas, foco)
mirada-protocol lib Contrato Cerebro↔Cuerpo: BrainCommand/BodyEvent + marco de cable
mirada-brain lib Orquestador del escritorio: Desktop, eventos→comandos, atajos
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)
mirada-ctl (app) bin/CLI Control externo del Cerebro (estilo swaymsg): acciones y consultas

Flujo

  mirada-layout ─► mirada-protocol ─► mirada-brain ─► [app mirada · Cerebro]
                          │                                  │
                          │                            mirada-link
                          │                                  │
                          └────► mirada-body ─► [mirada-compositor · Cuerpo]
  • El Cuerpo reporta hardware/clientes con BodyEvent (salida conectada, ventana abierta, atajo pulsado…).
  • El Cerebro (Desktop::on_event) recalcula y emite BrainCommand (Place con la geometría completa, Close, GrabKeys…).
  • El Cuerpo (BodyState::apply) traduce cada comando a BodyOp concretas y sólo emite lo que de verdad cambia.

Detalle por crate

  • mirada-layoutRect + split (reparto exacto de píxeles), LayoutMode con 7 modos (MasterStack, CenteredMaster, Spiral —espiral de Fibonacci—, Grid, Columns, Rows, Monocle) y LayoutMode::next() para el ciclo, Workspace con foco cíclico, reordenado y promote_focused. LayoutParams lleva master_ratio y master_count (nmaster); smart gaps (una sola ventana va a sangre). Determinista.
  • mirada-protocolWindowPlacement, los enums BrainCommand y BodyEvent, el marco postcard con prefijo u32 LE (write_frame/read_frame, guard MAX_FRAME) y el puente placements(&Workspace, Rect).
  • mirada-brainDesktop: salidas, 9 escritorios virtuales, registro de ventanas. on_event(BodyEvent) -> Vec<BrainCommand>; DesktopAction + Keymap configurable (set_keymap en caliente) + Rules de ventana. Multi-monitor: cada Output muestra un escritorio; relayout() tesela todas las salidas en un solo Place.
  • mirada-linkLink<Out,In> sobre socket Unix; hilo lector de fondo + canal mpsc para sondeo no bloqueante. BrainLink/BodyLink, connected_pair (socketpair), connect/listen por ruta.
  • mirada-bodyBodyState: salidas + superficies; apply traduce BrainCommandBodyOp (idempotente), los mutadores del backend devuelven los BodyEvent a mandar. Ejemplo headless: un Cuerpo sin gráficos guiado por stdin para ejercitar el bucle entero.
  • mirada (app) — envuelve Desktop y lo pinta (barra de escritorios + modo + foco, lienzo teselado). El lienzo dibuja todas las salidas a escala, cada una con su marco. Con MIRADA_SOCKET conecta a un Cuerpo; sin él corre en simulación (ventanas sintéticas, teclado de la propia ventana). Pips de escritorio y ventanas clicables.
  • mirada-ctl (app) — CLI de control: parsea la acción de los argumentos (DesktopAction: FromStr) y la manda al Cerebro por el socket de control; windows y actions para consultar.

Atajos de teclado configurables

El keymap vive sólo en el Cerebro (mirada-brain::Keymap). El Cuerpo nunca lo ve: recibe únicamente la lista de cadenas a interceptar en un GrabKeys, hace un Vec::contains ciego y devuelve la combinación pulsada como Keybind; es Desktop quien la traduce a DesktopAction. Esa separación —qué interceptar (lista barata, Cuerpo) vs. qué significa (el mapa, Cerebro)— hace innecesario cualquier candado o Arc: el mapa es monohilo y la lista se reemplaza de golpe.

  • Disco — RON de texto en ~/.config/mirada/keymap.ron, editable a mano y versionable. La app lo crea documentado en el primer arranque; si está corrupto, avisa y usa el de por defecto sin pisar el archivo.
  • Cable — sólo viaja la lista de cadenas (GrabKeys), vía el marco postcard que ya existe. No hay formato binario de configuración.
  • Vocabulario — la acción es una cadena estable ("focus-next", "layout:grid", "workspace:3"): DesktopAction: Display + FromStr.
  • Recarga en calienteKeymap::watch (sobre notify) vigila el archivo; al cambiar, el dueño del Desktop recarga, llama a set_keymap y reenvía el GrabKeys. Sin reiniciar.
  • Configurador — no hay ejecutable aparte: el editor de texto del usuario, y la app mirada (que a futuro puede dibujar un editor visual sobre el mismo API Keymap). cargo run -p mirada-brain --example keymap-default imprime el archivo por defecto.

API de acciones

Toda acción de escritorio converge en un único embudo: Desktop::apply(DesktopAction) -> Vec<BrainCommand>. El keymap no es más que un front-end (Keybindlookupapply); hay otros tres:

  • DesktopAction::FocusWindow(WindowId) — direccionamiento directo de una ventana (no sólo ciclar con FocusNext/Prev); si está en otro escritorio, salta a él. Lo usan la taskbar y mirada-ctl.
  • Ventanas flotantesToggleFloat (Super+f) saca la enfocada del teselado a un rectángulo libre (centrado, 60 %); Workspace guarda las flotantes aparte, layout() las pone al final y WindowPlacement /BodyOp::Configure llevan floating: bool para que el Cuerpo las componga por encima.
  • Pantalla completaToggleFullscreen (Super+Shift+f): la ventana cubre toda la salida (sin gap), oculta al resto y se lleva el foco; Workspace.fullscreen: Option<WindowId>, y el Cuerpo le fija el estado xdg_toplevel Fullscreen. También atiende la petición del propio cliente (xdg set_fullscreenBodyEvent::FullscreenRequest), así que un reproductor o un juego entran a pantalla completa solos.
  • Multi-monitor — cada Output muestra un escritorio distinto; SwitchWorkspace actúa sobre la salida enfocada (y la intercambia si el escritorio pedido ya lo muestra otra salida); FocusOutputNext (Super+o) mueve el foco entre monitores. El foco del teclado es único — sólo la ventana enfocada de la salida enfocada.
  • ScratchpadSendToScratchpad guarda la ventana enfocada (sale del teselado, en ningún escritorio); ToggleScratchpad (Super+`) la invoca flotando y centrada en el escritorio actual, o la oculta — estilo terminal desplegable. Desktop.scratchpad: Vec<WindowId>; mirada-ctl windows la lista como esc scratch.
  • Layout y área maestra por el API — los 7 modos se intercambian (SetLayout/CycleLayout, mirada-ctl layout spiral); el área maestra se redimensiona (grow/shrink-master, Super+l/Super+h); inc/dec-master cambian nmaster (Super+,/Super+.); y promote-to-master lleva la enfocada al puesto maestro (Super+Returncombo_string ya canoniza teclas con nombre: Return, Tab, F5…).
  • Lanzar programasDesktopAction::Spawn(String) (forma textual spawn:<comando>, Super+Shift+Returnspawn:foot por defecto) produce un BrainCommand::Spawn; el Cuerpo lo ejecuta con sh -c, y el hijo hereda WAYLAND_DISPLAY. DesktopAction deja de ser Copy por llevar el comando.
  • Lanzador de aplicacionesmirada-launcher (app aparte, sin dependencias): escanea los .desktop XDG y lanza el elegido desde una lista de terminal que se filtra escribiendo. El keymap ata Super+p a spawn:foot -e mirada-launcher.
  • HUD interactivo (app mirada) — los pips de escritorio y las ventanas del lienzo son clicables: clic = apply de la acción.
  • mirada-ctl — control externo por línea de comandos (mirada-ctl focus-next, workspace 3, windows). Habla con el Cerebro por un socket Unix aparte; el módulo mirada-brain::ctl define CtlRequest/CtlReply (marco postcard), CtlServer/CtlConn y send_request. El Cerebro (la app mirada siempre; mirada-compositor sólo embebido) abre el socket y atiende en su bucle. DesktopAction viaja como enum serializado: contrato tipado de punta a punta.

cargo run -p mirada-brain --example headless-ctl levanta un Cerebro sin gráficos para ejercitar mirada-ctl en modo desatendido.

Reglas de ventana

mirada-brain::rules decide qué hacer con una ventana al abrirse: config declarativa en RON (~/.config/mirada/rules.ron), mismo patrón que el keymap. Cada Rule casa por subcadena de app_id y/o title (sin distinguir mayúsculas; vacío = cualquiera) y aplica un destino: workspace (1..9) y/o floating. Gana la primera regla que case.

El Desktop consulta Rules::resolve en cada WindowOpened — el evento ya trae app_id/title — y manda la ventana a su escritorio, flotando si toca. Se carga al arrancar (la primera vez se escribe una plantilla con ejemplos comentados); las reglas afectan a las ventanas futuras, no a las ya abiertas.

Dependencias

  • Todos los lib con #![forbid(unsafe_code)]. Cero Wayland, cero smithay en los seis crates de arriba.
  • El acoplamiento a Wayland/hardware vive sólo en mirada-compositor.

Estado

Implementado y verde: mirada-layout (32 tests), mirada-protocol (11), mirada-brain (65), mirada-link (7), mirada-body (14), las apps mirada y mirada-compositor (compilan; verificación visual manual) y mirada-ctl (CLI, probado vía el ejemplo headless-ctl).

El Cuerpo ya existe: mirada-compositor es un compositor Wayland teselante real sobre smithay. Habla wl_compositor/xdg_shell/ wl_shm/wl_seat/wl_output/wl_data_device/xdg-decoration/ zwp_linux_dmabuf, compone las superficies de los clientes con GlesRenderer y aplica la geometría del Cerebro. Fuerza decoración ServerSide y no dibuja ninguna: las ventanas teseladas van sin marco (nada de barras de título de cliente). Con zwp_linux_dmabuf los clientes que pintan por GPU (apps GPUI, navegadores acelerados) pueden conectarse — el GlesRenderer importa sus búferes DMA-BUF al componer. Reusa mirada-body (contabilidad) y mirada-link (cable). Dos modos de Cerebro: autónomo (Desktop embebido) o enlazado (MIRADA_SOCKET → la app mirada).

Dos backends gráficos (main() elige; --winit/--drm lo fuerzan):

  • winit — corre anidado, una ventana en la sesión gráfica actual.
  • drm (drm_backend.rs) — corre nativo sobre una TTY, sin sesión anfitriona: libseat (sesión), udev (GPU), DrmDevice + GBM + EGL + DrmCompositor, libinput (teclado y ratón), bucle calloop. Verificado en hardware: sesión, render, teclado, atajos, clientes, salida limpia. El ratón: el cursor toma la superficie que pide el cliente (wl_pointer.set_cursorcursor_image) y cae a un cuadrado (SolidColorRenderElement Kind::Cursor) por defecto; el foco sigue al puntero (BodyEvent::PointerEntered) y clics y rueda van a la ventana debajo. Todo en un enum Frame de elementos de render. Super+arrastre mueve/redimensiona: el Cuerpo calcula el rectángulo y emite BodyEvent::WindowFloatTo { id, rect }; el Cerebro hace flotar la ventana ahí (Workspace::set_floating). Cada ventana lleva un marco de 2 px (4 SolidColorBuffer por ventana, Id estable): azul si tiene el foco, gris si no.
  • Conmutación de VTCtrl+Alt+Fn salta a otra TTY: el SessionEvent de libseat pausa el DrmDevice y suspende libinput; al volver, los reactiva, llama a DrmCompositor::reset_state y repinta. Mientras está cedida, render() no toca la GPU.
  • Sesión~/.config/mirada/autostart (un comando por línea) se lanza al arrancar el backend DRM; el script session/mirada-session y session/carmen.desktop integran carmen con un gestor de login.
  • Acople del shell — una ventana con app_id = "carmen.shell" no se tesela: carmen le reserva una franja de 40 px al pie de la salida y la pinta sobre todo. La reserva viaja como BodyEvent::OutputResized, que encoge el área útil del Cerebro sin perder el escritorio que muestra (a diferencia de quitar y volver a añadir la salida). Es el anclaje de la futura shuma-shell en modo launcher.

Pendiente — refinamientos del Cuerpo:

capa pendiente rol
puntero en winit ratón en el backend anidado (hoy sólo el backend DRM)
mirada-input repetición de teclas, gestos; hotplug de monitores
shuma-shell modo launcher: barra + input + cajón sobre el acople shell
wlr-layer-shell barras externas tipo waybar, fondos, notificaciones
mirada-sandbox aislamiento de clientes sobre arje-incarnate

CRIU (congelar/restaurar ventanas) queda anotado como futuro.