b17ac8c67a
Fase 3a del plan «shell»: `shuma-shell --launcher` (o la variable `MIRADA_SHELL`) arranca el shell como una barra compacta acoplada al pie de carmen, en vez del panel de 3 columnas. - `run_launcher` abre la ventana GPUI sin barra de título y con `app_id = "carmen.shell"` — el acople del compositor la reconoce y le reserva su franja. GPUI 0.2 admite `WindowOptions.app_id`. - `Shell.launcher: bool`; `Render::render` deriva a `render_launcher` cuando está activo: una barra de una línea — un glifo, la línea de comandos y el estado del último comando (en curso / ✓ / ✗). - La construcción de la fila del input (tokens coloreados + caret + sugerencia fantasma) sale a un helper `input_row` que comparten el panel completo y el modo launcher — sin duplicar el resaltado. `shuma-shell --launcher` va al `autostart.example`. Falta (3b/c/d): la barra de ventanas abiertas, el cajón de resultados y la config. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
255 lines
15 KiB
Markdown
255 lines
15 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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-layout`** — `Rect` + `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-protocol`** — `WindowPlacement`, 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-brain`** — `Desktop`: 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-link`** — `Link<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-body`** — `BodyState`: salidas + superficies; `apply` traduce
|
|
`BrainCommand`→`BodyOp` (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 caliente** — `Keymap::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 (`Keybind` → `lookup` → `apply`); 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 flotantes** — `ToggleFloat` (`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 completa** — `ToggleFullscreen` (`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_fullscreen` → `BodyEvent::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.
|
|
- **Scratchpad** — `SendToScratchpad` 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+Return`
|
|
— `combo_string` ya canoniza teclas con nombre: `Return`, `Tab`, `F5`…).
|
|
- **Lanzar programas** — `DesktopAction::Spawn(String)` (forma textual
|
|
`spawn:<comando>`, `Super+Shift+Return` → `spawn: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 aplicaciones** — `mirada-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_cursor` → `cursor_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 VT** — `Ctrl+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: falta la barra de ventanas y el cajón |
|
|
| `wlr-layer-shell` | barras externas tipo waybar, fondos, notificaciones |
|
|
| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` |
|
|
|
|
`shuma-shell --launcher` ya corre como el shell de carmen: abre una
|
|
ventana sin barra de título con `app_id = "carmen.shell"` (el acople la
|
|
reconoce) y dibuja una barra compacta — glifo, la línea de comandos de
|
|
`shuma-line` y el estado del último comando. Falta la barra de ventanas
|
|
abiertas (vía el socket de control) y el cajón de resultados.
|
|
|
|
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
|