gioser

tawasuyu/pata

Declarative desktop frame (bars/dock/tray/widgets), portable Linux/Wawa, on Llimphi.

⎇ 95bc028eeab35a55ae8bef41f820de73348d5baa 02_ruway/pata/SDD.md Markdown · 536 lines · 38.0 KB History raw

feat: pata standalone — marco de escritorio declarativo (barras/dock/tray/widgets) portable Linux/Wawa (front-door, git-dep al monorepo) 95bc028ee · Sergio ·

1# SDD — `pata`, el marco del escritorio
2
3> Estado: **Fase 8b** (layer-shell sobre wlroots: barras, Quake, clicks,
4> `window_list`). Este documento es la fuente autoritativa de qué es `pata` y
5> dónde termina, por encima de README.
6
7## 0. El problema que resuelve
8
9El escritorio de gioser tenía el concepto de "launcher" **triplicado y mal
10delimitado**: `mirada-launcher-llimphi` (la barra), `shuma-shell-llimphi` (un
11chasis con tabs) y `shuma-module-launcher` (un módulo lista-de-apps) competían
12por el mismo rol sin una frontera clara. Correr cualquiera bajo el compositor
13daba ventanas sueltas en vez de un escritorio coherente.
14
15`pata` fija la frontera: es **una sola capa**, la del *marco* (chrome) del
16escritorio, desacoplada del compositor y del shell, y portable entre Linux y
17wawa.
18
19## 1. Las tres capas (no se solapan)
20
21| Capa | Quechua/es | Qué es | Qué **no** es |
22|---|---|---|---|
23| **mirada** | mirar | El **compositor**: el Cuerpo Wayland/DRM. Tesela, acopla franjas, decora, enruta input. | No dibuja barras ni widgets. |
24| **shuma** | — | El **shell**: input + terminal + módulos. Se asoma como una barra inferior auto-escondible; al escribir, despliega el resto estilo Quake. | No es el chrome del escritorio; es un inquilino del marco. |
25| **pata** | borde, repisa, andén | El **marco**: barras, paneles y dock declarados desde un archivo de config, con widgets colocables en cualquier slot. Hospeda el input de shuma. | No compone ventanas (eso es mirada) ni ejecuta comandos (eso es shuma). |
26
27Regla mnemónica: **mirada** pone las ventanas, **pata** pone el marco
28alrededor, **shuma** es la boca por la que le hablás al sistema.
29
30## 2. Forma del dominio
31
32```
33pata-core      Modelo agnóstico (no_std + alloc): el esquema declarativo
34               (Config → [Surface] → slots → [WidgetSpec]) + el layout
35               (resolve: config+pantalla → superficies colocadas + work_area).
36               No pinta, no toca el SO. Cruza a wawa por `path`, como mirada-layout.
37pata-config    Loader Linux (std): lee el TOML del usuario desde las rutas XDG y
38               lo materializa en el modelo. El límite std→no_std del marco. Trae
39               el binario `pata` para inspeccionar la geometría resuelta. En wawa
40               este rol lo cumple akasha, no este crate.
41pata-llimphi   Frontend Linux: monta pata-core sobre Llimphi. Cada Surface es
42               una ventana Wayland que el compositor mirada acopla; despacha
43               los widgets builtin; el shuma_input despliega shuma. (Fase 3,
44               hereda de mirada-launcher-llimphi.)
45pata (wawa)    El kernel launcher de wawa consume el MISMO pata-core y pinta
46               sobre el framebuffer. (Fase 7.)
47```
48
49UIs intercambiables sobre un `*-core` agnóstico — la regla dura del repo. El
50modelo se escribe una vez; Linux y wawa son dos pinceles.
51
52## 3. El modelo (`pata-core`)
53
54- **`Config`** = `general` + `Vec<Surface>`. Múltiples superficies, no un único
55  panel. El usuario despliega tantas barras/paneles/docks como quiera.
56- **`Surface`** = `kind` (Bar | Panel | Dock) + `anchor` (Top/Bottom/Left/Right)
57  + `thickness` + `autohide` + tres slots `start`/`center`/`end` (+ `cards`
58  para paneles). Cada slot es una lista ordenada de widgets.
59- **`WidgetSpec`** = `kind: String` (conjunto **abierto**) + `props` arbitrarias.
60  El frontend despacha por string y cae a un placeholder si no conoce el kind:
61  agregar un widget no toca el core.
62- **`Prop`** = `Bool | Int | Num | Str`. Valor de propiedad **agnóstico del
63  formato en disco** — ni `toml::Value` ni nada atado a una plataforma. El
64  loader de cada SO (TOML en Linux, akasha en wawa) deserializa a esto.
65
66El formato en disco no es parte del modelo: en Linux un loader TOML deserializa
67directo a estos tipos vía `serde` (contrato fijado en `tests/toml_contract.rs`);
68en wawa el config llega por akasha.
69
70## 4. Widgets builtin previstos
71
72`start_button` · `window_list` (ventanas abiertas, vía mirada-ctl/-link) ·
73`clipboard` · `volume` · `brightness` · `tray` · `clock` · medidores
74(`ram_meter`/`cpu_meter`) · **`astro`** (posición zodiacal del sol + ciclo
75lunar, reusando `cosmos-ephemeris`) · `shuma_input` (el cabezal del shell).
76
77Cada uno se coloca libremente: superficie + slot se eligen desde el config.
78
79## 5. Integración con shuma (el Quake)
80
81El `shuma_input` es un widget que vive típicamente en una `Surface { kind: Bar,
82anchor: Bottom, autohide: true }`. Muestra el cabezal del shell; al recibir
83foco/escritura, el frontend **anima el despliegue** del resto de shuma sobre el
84escritorio (drawer estilo Quake) y lo repliega al soltar. El marco provee el
85borde; shuma provee el contenido.
86
87## 6. Estado y plan por fases
88
89- **Fase 1 ✅** — `pata-core` config: esquema declarativo, `Prop` agnóstico,
90  contrato TOML, `no_std` verificado (wasm32). En el workspace.
91- **Fase 2 ✅** — `pata-core::layout`: resuelve config+pantalla en superficies
92  colocadas + `work_area` (lo que mirada tesela). Geometría pura testeada.
93- **Fase 3 ✅** — `pata-config`: loader TOML/XDG → modelo + binario `pata`
94  inspector. Pipeline config→layout verificado sobre archivos reales.
95- **Fase 4 ✅** — modelo de widget agnóstico (`pata-core::widget`): trait
96  [`Widget`] (`tick(&WidgetCtx)` / `view() → WidgetView`), un `WidgetCtx` que el
97  host muestrea (reloj, cpu, ram, volumen, brillo) y un view-model
98  `Text | Meter | Placeholder | Empty` sin pincel. Builtins con lógica portada:
99  `clock` (strftime reducido), `cpu_meter` / `ram_meter` / `volume` /
100  `brightness` (medidor genérico). `build(spec)` despacha por string y cae a
101  `Placeholder` para kinds no implementados. `no_std` verificado (wasm32); el
102  inspector `pata --widgets` lo muestra de punta a punta.
103- **Fase 5 ✅** — `pata-llimphi`: el frontend Linux. `sampler` muestrea el
104  sistema (chrono + `/proc/stat` + `/proc/meminfo` + `/sys/class/backlight`) en
105  un `WidgetCtx`; `render` traduce cada `WidgetView` a `View<Msg>` (texto,
106  medidor con barra, placeholder tenue) y coloca las superficies en los rects
107  que el layout resolvió (posición absoluta). `PataApp` (app-id `gioser.pata`)
108  carga config vía `pata-config`, `tick`ea a 1 Hz y pinta. Por ahora una sola
109  ventana; mirada acopla por superficie en la Fase 8.
110- **Fase 6 (parcial)** — widgets nuevos:
111  - `astro` ✅ — posición zodiacal del Sol (signo + grado) + fase lunar. La
112    efeméride la computa el sampler (host) y la entrega en `WidgetCtx`
113    (`sun_longitude_deg` + `moon_phase`); el widget de core sólo mapea grados→
114    signo y fracción→fase, con aritmética entera (no_std). El sampler usa la
115    fórmula de baja precisión del *Astronomical Almanac*; `cosmos-ephemeris`
116    es el upgrade drop-in cuando se quiera alta precisión.
117  - `start_button` ✅ — muestra su `label` (default `⊞`). Cablear su acción
118    (abrir el lanzador) espera al ruteo de clicks (Fase 7).
119  - `window_list` ✅ (en layer-shell) — lista de ventanas abiertas vía el
120    protocolo `wlr-foreign-toplevel-management` (el que usan waybar/eww), no por
121    IPC de mirada. Ver el detalle en la Fase 8b. Bajo el compositor `mirada` (el
122    path winit) sigue vacío hasta que mirada exponga sus toplevels.
123  - `tray` ⏳ — StatusNotifierItem; diferido. Placeholder por ahora.
124- **Fase 7 ✅** — despliegue Quake de shuma desde `shuma_input`. El frontend
125  intercepta el kind `shuma_input` (es interacción, no pasa por el `build`
126  agnóstico de core, igual que mirada con su shuma_bar): un cabezal clicable en
127  la barra + hotkey (`keys`) despliegan un **drawer** animado (`llimphi-motion`,
128  scrim que cierra al click + panel inferior que crece con el tween) que captura
129  el teclado. El estado vive en `Model::shuma`, no en core. La ejecución del
130  comando es, estrictamente, de `shuma`: mientras no haya puente, `shuma::
131  ejecutar_stand_in` corre por `sh -c` como **sustituto temporal** (patrón de
132  mirada) — se reemplaza sin tocar el mecanismo del drawer.
133  - **Puente real + cards (✅)** — el drawer corre por `shuma-exec` (no `sh -c`
134    pelado): historial de *cards* (`$ cmd` + etapas + salida + código), plegables,
135    con scroll. **Captura por etapa (tee, paridad con el shell de shuma):** un
136    pipe «simple» (sólo comandos/args/flags y `|`, sin comillas/variables/
137    redirecciones/globs/`~`) corre por `Exec::Direct` con `capture_stages`; cada
138    etapa **intermedia** emite su stdout en vivo (`StageStdout`) y se guarda en
139    `DrawerBlock::stage_lines`. Clickear la chip de una etapa intermedia **revela
140    su salida capturada inline** (sin re-ejecutar); la última etapa no se captura
141    aparte (su stdout es el cuerpo de la card). Cualquier otra sintaxis cae a
142    `sh -c` (sin tee). Detección en `shuma::simple_pipe_stages` (espeja
143    `shuma-module-shell`), testeada.
144  - **Submit a IA (✅, paridad con el quake de mirada-launcher)** — el buffer sin
145    prefijo va al **LLM** (`pluma-llm::from_env`, cae a Mock sin credenciales); el
146    prefijo `!`/`$` lo fuerza a shell. `shuma::classify` decide (`Empty`/`Shell`/
147    `Ia`, testeada); las consultas IA abren una card `✦ <prompt>` sin chips de
148    etapa que muestra `…pensando` y luego la respuesta. El resultado llega por el
149    mismo `ShumaResult`/`finish_last` que un comando. Es el último gap que tenía
150    `mirada-launcher-llimphi` sobre pata de cara a la Fase 10.
151- **Fase 8 ✅** — `mirada-compositor` reconoce el marco `pata`:
152  - Identidad: el viejo `SHELL_APP_ID = "carmen.shell"` → `is_shell_app_id`, que
153    matchea `gioser.pata` (la identidad que anuncia `pata-llimphi`) o el alias
154    legacy `carmen.shell`, override por `MIRADA_SHELL_APP_ID`.
155  - Anclaje/grosor configurables (`MIRADA_SHELL_ANCHOR` / `MIRADA_SHELL_THICKNESS`,
156    defaults bottom/40), ya no una franja fija de 40px al pie. Geometría en
157    helpers puros testeados (`shell_strip` / `shell_insets`).
158  - **Zonas exclusivas en los cuatro bordes**: el acople ya no encoge la salida,
159    sino que reserva *insets* (top/bottom/left/right) vía el evento nuevo
160    `BodyEvent::OutputReserved` → `Body::reserve_output` → el Cerebro guarda
161    `Output::reserved` y tesela sobre `Output::work_rect()` (rect menos insets).
162    El motor de layout ya respetaba `screen.x/y`, así que top/left desplazan el
163    origen del teselado correctamente. Soporta barras en varios bordes a la vez.
164    Cerrar el shell libera la reserva (insets en cero).
165- **Fase 8b (en curso)** — `pata` como **layer surface** (nivel eww/waybar) en
166  compositores wlroots (Hyprland, Sway, river), no como ventana cliente. Sin
167  esto, en Hyprland pata abría como ventana flotante; el acople de la Fase 8 era
168  sólo para el compositor `mirada`, no para terceros.
169  - `llimphi-hal::RawSurface` — `Surface` sobre una `wgpu::Surface` creada desde
170    handles raw, **sin `winit::Window`** (misma intermedia + blit que
171    `WinitSurface`). Es la costura: el render de Llimphi ya era winit-free salvo
172    la creación de la surface.
173  - `pata-llimphi::layer` — backend `wlr-layer-shell` con
174    `smithay-client-toolkit`: crea **una layer surface por cada superficie
175    `Bar`** de la config (cada una anclada a su borde + `set_exclusive_zone`),
176    saca su `wgpu::Surface` de los punteros `wl_display`/`wl_surface`, y la pinta
177    reusando `mount → compute → paint → render` vía [`render::bar_view`]. Un
178    `Hal` (instancia/device de wgpu) compartido; estado wgpu por panel
179    (`PanelGpu`). Muestreo 1Hz compartido + flag `dirty` por panel (no
180    re-rasteriza a 60fps). `main` elige layer-shell si hay `WAYLAND_DISPLAY`
181    (salvo `PATA_BACKEND=winit`), con fallback a la ventana winit.
182    Verificado en runtime (Hyprland): salen todas las barras ancladas, sin
183    error, y el muestreo/leyenda quietos.
184    - **Gotcha Vulkan WSI + smithay (mirada)** — `draw` redimensiona la surface
185      en cada cuadro (no hay evento de resize como en winit), así que
186      `RawSurface::resize` es **no-op cuando el tamaño no cambia**: reconfigurar
187      el swapchain por cuadro reconstruye el `wl_buffer` y destruye el recién
188      presentado antes de que el compositor lo componga — wlroots lo tolera,
189      smithay (mirada) no, y la barra quedaba negra (`buffer=None`). `acquire`
190      reconfigura+reintenta una vez ante `Outdated`/`Lost`. Fix en `b8747b90`.
191  - **Input + Quake** ✅ (verificado en Hyprland): seat/keyboard/pointer
192    vía sctk. Un cliente layer-shell **no recibe hotkeys globales**, así que el
193    Quake se abre con **click** en la barra de shuma (foco de teclado vía
194    `OnDemand` → al abrir pasa a `Exclusive`). En vez de una segunda surface, la
195    propia barra de shuma **crece hacia arriba** hasta `DRAWER_H` (su exclusive
196    zone queda en el grosor de la barra, así no recoloca el teselado);
197    `render::shuma_open_view` pinta el cuerpo del drawer (input + salida) arriba
198    y el cabezal abajo. Teclado con foco: Esc cierra, Backspace, Enter ejecuta
199    (`shuma::ejecutar_stand_in`, `sh -c` bloqueante), texto → buffer.
200  - **Clicks por hit-test** ✅ — cada panel guarda su árbol pintado
201    (`RenderCache`: `Mounted` + `ComputedLayout`); al click, `hit_test_click`
202    ubica el nodo bajo el puntero y dispara su `on_click` (vía `handle_msg`). El
203    cabezal `› shuma` togglea con precisión (abre y cierra); clickear el reloj o
204    un medidor no hace nada. Reemplaza al "cualquier click en la barra abre".
205  - **Acciones por widget** ✅ — cualquier widget con una prop `exec` se vuelve
206    clickeable (estilo waybar): `SlotWidget::Core { widget, exec }` lleva el
207    comando; el render le pone `on_click(Msg::Spawn(cmd))` + `hover_fill`; ambos
208    backends lo lanzan con `spawn_cmd` (`sh -c`, sin esperar). Ej. en el asset:
209    `start_button` con `exec = "wofi --show drun"`.
210  - **Exec asíncrono del Quake** ✅ — el `Enter` corre el comando en un hilo y el
211    resultado llega por un `mpsc::Receiver` que el latido sondea (`try_recv`) cada
212    frame; ya no bloquea el loop. Mientras corre, el drawer muestra `…`.
213  - **Volumen real** ✅ — el sampler lee el volumen del sink por defecto vía
214    PipeWire (`wpctl get-volume`) con fallback a PulseAudio (`pactl`), y rellena
215    `WidgetCtx::{volume, muted}` (el brillo ya lo lee de `/sys`). El medidor deja
216    de marcar 0%. Parseo en funciones puras testeadas (`parse_wpctl`/
217    `parse_pactl_pct`). Bonus en el asset: `volume` con `exec = "pavucontrol"`.
218  - **`window_list`** ✅ — la lista de ventanas abiertas, vía
219    `wlr-foreign-toplevel-management` (`wayland-protocols-wlr`), el protocolo de
220    waybar/eww. El manager se bindea opcional (si el compositor no lo expone, el
221    widget queda vacío sin romper); cada toplevel acumula título/app_id/estado en
222    `pata-llimphi::toplevel::Toplevel` y se confirma en `done`. El render pinta un
223    chip clickeable por ventana (la activa resaltada); el click manda
224    `Msg::ActivateWindow(id)` → `activate(seat)`, que la trae al frente. Como el
225    `shuma_input`, es interacción + IPC: no pasa por el `build` agnóstico de core
226    sino que lo intercepta el frontend (`SlotWidget::WindowList`); los datos se
227    pasan al render aparte del view-model. El asset `launcher.toml` ya lo tiene en
228    el centro de la barra superior.
229  - **`clipboard`** ✅ — preview del texto copiado. El sampler lo lee con
230    `wl-paste --no-newline --type text/plain` (subproceso ~1Hz, como el volumen
231    con `wpctl`) y lo colapsa a una línea (`sampler::preview_clipboard`, testeada);
232    el render pinta `📋 <preview>` recortado. Como `window_list`, es dato del host
233    interceptado por el frontend (`SlotWidget::Clipboard`), no view-model de core;
234    los datos del host (ventanas + portapapeles) viajan juntos en `render::BarData`.
235    Una prop `exec` lo vuelve clickeable → selector de historial (en el asset:
236    `cliphist list | wofi --dmenu | cliphist decode | wl-copy`). Sólo en
237    layer-shell (el path winit pasa `BarData::default()`).
238  - **`tray`** ✅ — la bandeja del sistema (StatusNotifierItem). pata corre como
239    **watcher + host**: posee `org.kde.StatusNotifierWatcher` y atiende a las apps
240    que registran su item. Como el bucle sctk es bloqueante y zbus es async, el
241    tray vive en su **propio hilo** con un runtime tokio current-thread (el
242    workspace fija zbus con la feature `tokio`, no la blocking — patrón de
243    `mirada-portal`); comparte el snapshot de items por `Arc<Mutex>` y recibe los
244    clicks por un canal tokio (como el exec del Quake). El render pinta un chip por
245    item (resaltando `NeedsAttention`); el click manda `Msg::TrayActivate(key)` →
246    `Activate(0,0)` por D-Bus. Interceptado por el frontend (`SlotWidget::Tray`),
247    los items viajan en `render::BarData`. **Íconos** ✅: resuelve el `IconPixmap`
248    (ARGB32 por D-Bus → RGBA, sin tema) y, si no, el `IconName` como PNG en los
249    dirs estándar (hicolor + pixmaps, sólo PNG, sin `index.theme` ni SVG); cae a
250    texto si nada resuelve. El hilo del tray decodifica a `TrayIcon{rgba}` y el
251    render lo envuelve en `peniko::Image` (`View::image`, 18px). **No** emite
252    señales del watcher ni hace fallback si ya hay un watcher (si el nombre está
253    tomado, queda vacío y loguea). `split_service`
254    normaliza el registro (ruta+remitente / nombre de bus / combinado), testeada.
255    El tray sólo arranca si la config declara un widget `tray`. Ver `02_ruway/pata/
256    pata-llimphi/src/tray.rs`.
257  - **Fase 6 cerrada**: todos los widgets previstos (§4) existen, con íconos
258    reales en el tray. **`clipboard` y `tray` cableados también en el path winit**
259    (el `Model` muestrea el portapapeles cada tick y arranca el `TrayHandle` si la
260    config lo pide; `render::root` arma el `BarData` desde el `Model`). El único
261    pendiente es **`window_list` bajo el path winit/mirada**: necesita el cliente
262    foreign-toplevel (que vive en el backend layer-shell) o el IPC de toplevels de
263    mirada; hasta entonces queda vacío en ese path. Helper `config_tiene_widget`
264    compartido por ambos backends para arrancar el tray sólo si hace falta.
265- **Fase 8c — pulido de escritorio** (en curso):
266  - **Gradiente en los medidores** ✅ — la barra de relleno de cpu/ram/volumen/
267    brillo pinta un gradiente lineal (acento → acento aclarado) con `paint_with`
268    (Llimphi sólo tiene fill de color sólido). Ambos backends.
269  - **Task manager estilo KDE** ✅ — el `window_list` pasa de chips planos a
270    botones con ícono-badge (inicial del `app_id`) + título; la activa resaltada,
271    las minimizadas atenuadas. Clic izq. activa o **minimiza** si ya estaba activa;
272    clic der. **cierra** (`Msg::CloseWindow` → `handle.close()`). `Toplevel`
273    trackea `minimized`; el pointer del layer-shell rutea `BTN_RIGHT`.
274  - **Tarjetas flotantes (conky)** ✅ — `SurfaceKind::Panel` + `FloatingCard` ya
275    estaban en el modelo; ahora `render::card_view` las pinta y el layer-shell crea
276    **una layer surface por tarjeta** en `Layer::Bottom` (sobre el escritorio,
277    anclada a la esquina sup-izq con margen (x,y), sin reservar franja ni teclado).
278    En winit se pintan en absoluto. Asset con una tarjeta `sistema`.
279  - **Botón de inicio con menú nativo** ✅ — el `start_button` despliega un menú
280    de apps del registro (`app-bus AppRegistry::discover`). En layer-shell la barra
281    superior crece hacia abajo (truco del drawer Quake, al revés); en winit sale
282    por `view_overlay`. `exec` en el spec lo deja delegando a un lanzador externo.
283  - **Hover en todos los widgets** ✅ — el layer-shell pasaba `None` a `paint`, así
284    que `hover_fill` estaba muerto; ahora trackea `Motion`/`Leave` →
285    `hit_test_hover` → `hover_idx`. Todos los widgets dan realce al pasar el cursor.
286  - **Tooltip flotante (texto)** ✅ — cada widget lleva su tooltip vía el primitivo
287    nuevo `View::tooltip` de Llimphi (medidores: etiqueta + leyenda; ventanas/tray/
288    clipboard: el texto completo, útil cuando la barra lo recorta). El layer-shell
289    crea una **layer surface dedicada en `Overlay`** con región de input vacía (no
290    roba puntero); al cambiar el nodo hovereado, `update_tooltip` lee texto + rect
291    del cache de hit-test y reubica la surface bajo el widget (`set_margin`/
292    `set_size`); al salir se oculta fuera de vista. Cajita opaca (no depende de
293    transparencia de surface). Runtime a validar en compositor (norma de pata).
294- **Fase 9 ✅** — kernel launcher de wawa sobre `pata-core`. El kernel
295  enlaza `pata-core` por `path` (`default-features = false`, como mirada-layout) y
296  consume el **mismo** modelo de widgets que el frontend Llimphi: `compositor::
297  pata_marco` arma un `WidgetCtx` desde los datos del kernel (la RAM real del heap,
298  vía `memory::allocator::stats`), construye los widgets (`build_all`), los
299  `tick`ea y traduce cada `WidgetView` a las primitivas del framebuffer
300  (`grafico::Lienzo` + `texto::rasterizar`). Hoy pinta el cluster de indicadores
301  del taskbar (medidor de RAM) a la izquierda del reloj — un modelo, dos pinceles,
302  sobre bare-metal. Compila con `cargo +nightly check --target x86_64-unknown-none
303  -Z build-std=core,alloc`; runtime a validar en QEMU (norma de wawa).
304  - **`pata_core::resolve` integrado** — `pata_marco` ahora arma un `Config`
305    (barra de menú superior con `start_button` + `ram_meter`), lo resuelve con la
306    geometría canónica `resolve` (la misma que en Linux) y pinta **cada barra
307    resuelta** con sus tres slots (start izquierda / center centrado / end
308    derecha) en su rect. Se llama desde `consola::recomponer` sobre el área de
309    apps, tras componer el escritorio. El cluster suelto del taskbar se reemplazó
310    por esta barra completa.
311  - **Input al `start_button` cableado** — `pata_marco::start_button_rect(area)`
312    resuelve el mismo `Config` y devuelve el rect clickeable del ⊞ (espejando
313    dónde lo pinta `pintar_barra`); el ratón del compositor (`raton::atender_raton`)
314    detecta el clic ahí y **abre el launcher** (el mismo gesto que `Alt+P`), antes
315    de tocar foco/arrastre. El picker Spotlight ya existente se reusa tal cual.
316  - **Config por akasha** — el config del marco viaja por el grafo
317    direccionado por contenido, no armado en memoria. Como el modelo está afinado
318    para TOML (`WidgetSpec.props` con `flatten`, `Prop` `untagged`) y eso rompe
319    postcard (el codec de akasha, no auto-descriptivo), `pata-core` ganó un espejo
320    **postcard-safe**: `pata_core::wire::WireConfig` (props como lista ordenada,
321    `WireProp` etiquetado), con conversiones sin pérdida `Config ↔ WireConfig`
322    (round-trip por postcard fijado en un test del host). El kernel
323    (`pata_marco::marco`) serializa el default a `WireConfig`, lo **graba en el
324    grafo** (`almacen::almacenar`, BLAKE3 + postcard) y lo **lee de vuelta** —el
325    config hace el round-trip completo por akasha—, con fallback al default y
326    cacheado tras el primer uso.
327  - **Franja reservada** — el compositor ya **reserva** la franja de la barra de
328    menú: `area_apps` (la región que se tesela) descuenta `pata_marco::ALTO_BARRA`
329    además de las franjas de consola/taskbar, así las ventanas tilean **debajo**
330    de la barra (el equivalente al `Frame::work_area` de resolve). `region_barra_
331    marco` deriva la franja una sola vez; el render la pinta ahí y el ratón
332    hit-testea el `start_button` ahí — sin drift entre reservar, pintar y clickear.
333  - **Propuesta de config desde userspace** — la capacidad WASM
334    `sys_marco_proponer(ptr, len)` (en `wasm/env/config.rs`, gateada por
335    `PERMISO_CONFIG` + foco, espejo de `sys_config_proponer`) recibe un
336    `WireConfig` postcard de la app, lo valida, lo graba en el grafo y reemplaza
337    el marco activo (`pata_marco::proponer`) — el config por akasha es
338    bidireccional. El cache es un `Mutex<Config>` que la propuesta reescribe.
339  - Cierre: el launcher de wawa corre sobre el MISMO `pata-core` que Linux —
340    declarativo, resuelto por `resolve`, con widgets, render al framebuffer,
341    input al `start_button`, y config por akasha (lectura + escritura).
342  - **Refino: reserva dinámica ✅** — `area_apps`/`region_barra_marco` ya no usan
343    una constante: leen `pata_marco::alto_reservado()`, la suma de los grosores de
344    las barras `Bar` superiores no-`autohide` del config **resuelto**. Si una app
345    propone (vía `sys_marco_proponer`) una barra de otro alto, la reserva, el
346    render y el hit-test la siguen sin drift.
347  - **Refino: persistir el marco entre reinicios ✅** — el marco activo ahora se
348    ancla en el manifiesto, como `configuracion`/`overlay_revocacion`.
349    `format::Manifiesto` ganó `marco: Option<Hash>` (VERSION_MANIFIESTO 6→7); el
350    génesis (`wawa-boot`) nace con `marco: None`. Al arrancar, `pata_marco::
351    cargar_inicial` lee el marco del manifiesto si está anclado, o siembra el
352    default en el grafo y lo reancla (`manifiesto::enlazar_marco`). `proponer`
353    reancla al nodo nuevo, así un marco propuesto sobrevive al reinicio. Seguro
354    porque el génesis local **no se verifica por firma al boot** (confirmado por
355    el autor) y el operador re-forja la imagen en cada `cargo run -p boot`, así
356    que el bump de versión nace limpio.
357- **Fase 10 ✅** (2026-06-03) — `mirada-launcher-llimphi` **retirado**: pata cubre y
358  excede su rol (shell+tee+IA, task manager KDE, tarjetas conky, menú de inicio
359  nativo, tooltips, reloj UTC). Se borró el crate, se sacó del workspace y se
360  limpiaron las referencias (scripts/install-mirada-dm.sh, APPS.md, README de
361  mirada, REPORTE de shuma, LEEME de launcher-llimphi). El triplete de launchers
362  del §0 queda resuelto: el marco es **una sola capa**, `pata`.
363- **Fase 11 — sidebars acoplables (navegador de Mónadas/archivos)** (en curso):
364  el marco gana un cuarto tipo de superficie, el **Sidebar**, para integrar el
365  plano de datos de `chasqui`/nouser en el escritorio.
366  - **11a ✅ (modelo, `pata-core`)** — `SurfaceKind::Sidebar`: un **rail de
367    dientes** (`llimphi-widget-dock-rail`) anclado a un borde vertical (left/
368    right). Cada diente es un `SidebarTab { icon, label, content: WidgetSpec }`;
369    `content` es típicamente `kind = "navigator"`. Campos nuevos en `Surface`:
370    `tabs: Vec<SidebarTab>` + `panel_width` (el rail usa `thickness`, el panel
371    desplegado flota a su lado con `panel_width`). **Layout:** el rail reserva su
372    grosor como una barra vertical (salvo `autohide`, que flota); el panel que
373    despliega un diente **no** entra en `resolve` —flota sobre el área de trabajo
374    como un drawer de launcher, lo maneja el frontend—. Espejo postcard-safe
375    (`WireTab` + `tabs`/`panel_width` en `WireSurface`) para que viaje por akasha
376    en wawa; round-trip fijado en test. `no_std` (wasm32) verde, 36 tests.
377  - **11b-1 ✅ (protocolo nouser, `chasqui`)** — el navegador necesita el nivel
378    de archivos, y nouser es la **fuente autoritativa** de qué archivos componen
379    una Mónada (no el filesystem por su cuenta — decisión del autor). `chasqui-
380    card::query` gana `QueryRequest::ResolveMonad{id}` + `ResolveMonadResponse{
381    monad, members}` + `FileView` slim; `client::resolve_monad` (round-trip
382    extraído a un `request<R>()` genérico compartido con `list_monads`); el
383    `engine_socket` lo sirve mapeando `db.resolve_members(id) → FileView`. Tests
384    de roundtrip + servidor.
385  - **11b-2 ✅ (widget navegador Llimphi)** — `llimphi-widget-navigator`
386    **reutilizable y data-agnóstico**: bosque de `NavNode{id:u64,label,kind:
387    NavKind,children}` (kind = Monad/Group/Dir/File/Other) + dos modos
388    conmutables `NavMode::{Tree,Graph}`. **Árbol** reusa `llimphi-widget-tree`
389    (icono vectorial por kind, chevron toggle, click select, right-click
390    context). **Grafo** reusa `llimphi-widget-nodegraph` (layout en columnas por
391    profundidad, cables de contención padre→hijo, arrastrar selecciona, nodo
392    seleccionado resaltado por tint). Render-only: el estado (expanded/selected/
393    mode) vive en el caller. `navigator_view(spec, is_expanded, on_toggle,
394    on_select, on_context)`. Demo `navigator_demo` con toggle segmentado; 4
395    tests. El widget no sabe de nouser — lo alimenta pata.
396  - **11c ✅ (path winit, `pata-llimphi`)** — el frontend integra el plano de
397    datos de nouser y pinta el sidebar:
398    - **Plano de datos** (`nouser.rs`): descubre el socket del daemon (broker
399      brahman → fallback al default path, igual que `chasqui-explorer-llimphi`),
400      poll periódico de `list_monads` (2 s) y `resolve_monad` **bajo demanda** al
401      expandir una Mónada (carga perezosa: una Mónada con `cardinality > 0` aún
402      sin resolver lleva un hijo placeholder "…" para mostrar el chevron). El
403      `NavId` se deriva determinista del `MonadId`/path (FNV-1a con tag) para que
404      expansión y selección sobrevivan al re-poll. `NavState` (open/mode/selected/
405      expanded/scroll/roots/targets) vive en el `Model`; queries en thread vía
406      `Handle::spawn` (no bloquean el UI). 7 tests.
407    - **Render** (`render/sidebar.rs`): el rail (`llimphi-widget-dock-rail`) se
408      pinta en el rect que el layout reservó para el Sidebar, un diente por
409      `SidebarTab` (el del panel desplegado va resaltado). El panel **flota**
410      junto al rail (no entra en `resolve`): cabezal con el toggle Árbol/Grafo
411      (`llimphi-widget-segmented`) + el navegador (`llimphi-widget-navigator`)
412      dentro de un área de scroll (`llimphi-widget-scroll`). Clic en diente →
413      despliega/repliega; Esc cierra el panel. Iconos de diente vectoriales por
414      nombre (`monads`/`files`/…). 1 test.
415    - **Config**: el asset `pata-config/assets/launcher.toml` gana un sidebar de
416      ejemplo (`kind = "sidebar"`, un diente `navigator` source=nouser);
417      deserialización fijada en `toml_contract.rs`.
418    - Sólo arranca el poll si la config declara un navegador
419      (`config_tiene_navigator`).
420  - **11c-layer ✅ (runtime sin verificar, `layer.rs`)** — el rail/panel bajo
421    `wlr-layer-shell`, el path de producción en Hyprland:
422    - Una layer surface por Sidebar, anclada al borde vertical con exclusive zone
423      = `thickness`. Al activar un diente la surface **crece en ancho** (a
424      `thickness + panel_width`) manteniendo la exclusive zone, así el panel flota
425      sobre el área de trabajo sin recolocar el teselado — el truco del drawer de
426      shuma, pero en el eje horizontal. `set_sidebar_open` redimensiona +
427      invalida el cache de hit-test; `sidebar_surface_view` (render-fill, en
428      `render/sidebar.rs`) ordena rail+panel según el anclaje.
429    - **Plano de datos**: un hilo poolea `list_monads` cada 2 s y entrega por
430      canal (patrón sampler/exec); `resolve_monad` en hilos one-shot por otro
431      canal; `poll_nav` los drena cada frame sin bloquear. Sólo arranca si la
432      config declara un navegador.
433    - **Clics**: el hit-test del pointer handler ahora cae a `on_click_at`/
434      `on_right_click_at` (con coords locales al nodo) además de `on_click` —
435      necesario para los dientes del rail (que usan `on_click_at` para coexistir
436      con drag); paridad con el bucle winit. Navegación 100% por clic (sin
437      teclado): el panel se cierra re-clickeando el diente.
438    - **Limitación**: el modo **grafo** selecciona por arrastre (el nodegraph usa
439      `draggable`), que el backend layer-shell no rastrea aún → en layer-shell el
440      grafo es de sólo lectura; el modo **árbol** (default) funciona completo.
441      Compila; runtime se itera en el Hyprland del usuario (headless no verifica).
442  - **11d ✅** — abrir un archivo con la app que corresponda. El right-click sobre
443    un archivo (`Msg::NavOpen`) enruta por `open::open_file` (módulo `open.rs`) en
444    ambos backends: deriva el mime de la extensión (tabla acotada `mime_for_path`, sin
445    leer disco), busca una app nativa que lo declare (`app_bus::AppRegistry::
446    handlers_for(mime)` → `AppEntry::open(path)` con sustitución freedesktop
447    `%f`/`%u`), y si ninguna lo maneja cae a `xdg-open` (que respeta las
448    asociaciones del escritorio). Las apps de la suite tienen prioridad sobre el
449    handler del sistema. 3 tests del resolutor de mime. (mirada no expone API de
450    apertura —es WM puro—; spawnear el proceso es la vía, como en
451    `chasqui-explorer`.) Manifiestos de ejemplo de apps reales de la suite en
452    `shared/app-bus/assets/apps/` (`media.toml` para video/audio, `nada.toml` para
453    texto/código); se copian a `~/.config/gioser/apps/`. La decisión de ruteo
454    (`open::handler_for`) es pura y testeada; el formato de manifiesto tiene
455    canario en `app-bus`.
456  - **11d-extra ✅** — menú "Abrir con…" para elegir el handler. El right-click
457    sobre un archivo (`Msg::NavContextMenu`) precomputa sus apps nativas
458    (`open::handlers_for_path`, guardadas en `NavState::menu_options`) y abre un
459    selector **dentro del panel** (no un overlay flotante: así no necesita coords
460    del cursor y funciona idéntico en winit y layer-shell). Cada fila →
461    `Msg::NavOpenWith(id, Some(app_id))`; "el sistema" → `NavOpenWith(id, None)`
462    (xdg-open); "Cancelar"/Esc → `NavMenuCancel`. El render lee sólo `NavState`
463    (decoplado del registro). 2 tests del listado de handlers.
464  - **drag en layer-shell ✅** — el pointer handler del backend layer-shell rastrea
465    un drag mínimo (press→move→release) e invoca el handler `draggable` del nodo
466    bajo el cursor: en `Press` sobre un nodo arrastrable arranca el drag (no lo
467    trata como click), en `Motion` le pasa el delta (`Move`), en `Release` emite
468    `End`. Así el modo **grafo** del navegador selecciona también bajo Wayland (el
469    nodegraph selecciona al soltar) — ya no es de sólo lectura ahí. `LayerDrag`
470    guarda el handler + última posición.
471  - **Pendiente opcional**: discernimiento por contenido (`shuma-discern`) como
472    upgrade del mime por extensión (a costa de leer una muestra del archivo).
473
474- **Fase 12 — rail hospedado (sidebars de apps en el rail global)**:
475  una app puede **delegar su sidebar** al marco: cuando tiene foco, sus "dientes"
476  aparecen en el rail de pata (debajo de los propios) y su ventana queda como puro
477  lienzo; al clickear un diente, el comando vuelve a la app, que muestra ese panel
478  sobre su canvas. pata sólo hospeda el **rail** —no los paneles ricos de la app—.
479  - **Protocolo `pata-host`** (`02_ruway/pata/pata-host`): socket Unix dedicado
480    (`$XDG_RUNTIME_DIR/pata-sidebar.sock`, override `PATA_SIDEBAR_SOCKET`), marco
481    postcard con prefijo de longitud. `AppMsg::{Register{app_id,title,teeth},
482    Update,Bye}` (app→shell) + `ShellMsg::Activate{tooth}` (shell→app);
483    `HostedTooth{id,icon,label}`. `HostServer` (lado pata: acumula registros por
484    `app_id` en hilos lectores; `snapshot`/`activate`/`revision`) + `HostClient`
485    (lado app: registra, hilo lector entrega activaciones por callback, Drop manda
486    Bye). 5 tests.
487  - **Host en `pata-llimphi`** (sólo layer-shell, que conoce el foco): arranca el
488    `HostServer` si hay algún sidebar; `focused_app_id()` = toplevel activo; el
489    rail del sidebar muestra `host.snapshot(app_id)` de la app enfocada (segundo
490    `dock-rail` bajo los dientes de la config, con separador); clic →
491    `HostToothActivate(app_id,tooth)` → `host.activate`. `poll_host` re-pinta al
492    cambiar la revisión del host. El diente hospedado no abre panel ni redimensiona
493    pata: es control remoto del canvas de la app. El hit-test del pointer ya cae a
494    `on_click_at`, que estos dientes usan.
495  - **Integración cosmos** (opt-in `COSMOS_DELEGATE_SIDEBAR`): `app_id()=
496    "gioser.cosmos"`; publica sus `DockItem`s como dientes; `Msg::HostActivate`
497    togglea el panel correspondiente sobre su canvas; en modo delegado no pinta sus
498    rails (`dock_rail_overlay`→None) y un panel aparece sólo si su lado está
499    expandido → sin nada activo, puro canvas.
500  - **Requisitos runtime**: pata corriendo en layer-shell con un sidebar en la
501    config; cosmos lanzado con `COSMOS_DELEGATE_SIDEBAR=1`. Sin verificar headless.
502  - **media y pluma también delegan** (reusan el mismo `pata-host`):
503    - **media** (`MEDIA_DELEGATE_SIDEBAR`, `app_id="gioser.media"`): dientes
504      Config/Cola/Visualizadores/Ayuda; `Msg::HostActivate` despacha los Msgs de
505      toggle existentes (Config/Cola/Ayuda son ventanas/overlay) o togglea el flag
506      de visualizadores. media ya es canvas (no tiene rail propio que ocultar).
507    - **pluma** (`PLUMA_DELEGATE_SIDEBAR`, `app_id="gioser.pluma"`): dientes
508      Documentos/LLM/Buscar/Diff. Cambio **aditivo**: en modo delegado las columnas
509      laterales se vuelven colapsables (`side_izq_visible`/`side_der_visible`; cada
510      lado oculto sale del árbol con su splitter) → editor a pantalla completa;
511      Buscar/Diff reusan su lógica. Sin delegar, el layout de 3 columnas es idéntico.
512    - **shuma** (`SHUMA_DELEGATE_SIDEBAR`, `app_id="shuma.shell"`): un diente por
513      tab (id = índice → `Msg::HostActivate` selecciona esa tab) + un diente
514      "Monitores" (id sentinela `u32::MAX` → togglea el panel derecho). Cambio
515      **aditivo**: en modo delegado el panel de monitores arranca oculto (puro
516      lienzo) y el contenido toma todo el ancho sin splitter; el rail de pata lo
517      despliega. Sin delegar, el panel de monitores siempre se ve (`monitors_visible`
518      arranca según `host.is_none()`). La tira de tabs local sigue visible (el rail
519      es un switcher paralelo).
520  - **shuma embebida (in-process, NO socket)**: cuando el marco hospeda un
521    `shuma_input` (el drawer Quake; `ShumaState::present`), el rail del sidebar
522    suma un **tercer grupo** de dientes (bajo config-teeth y hosted-teeth, con
523    separador): un diente que despliega/repliega el drawer (`Msg::ShumaToggle`).
524    Al vivir en el propio proceso de pata —no detrás del socket `pata-host`—, el
525    diente **refleja el estado real** (`active = shuma.open`, a diferencia de los
526    hospedados, siempre inactivos) y **no depende del foco**: aparece igual en
527    winit y layer-shell mientras la config declare el `shuma_input`. Implementado
528    en `render::sidebar::{shuma_rail, rail_strip}` (firma de `sidebar_rail_view`/
529    `sidebar_surface_view` enhebra `&ShumaState`); icono `shell`/`terminal` =
530    glifo Group. Esto es la contraparte in-process de la delegación por socket:
531    cruzar la frontera de proceso (app aparte) usa `pata-host`; embebido se cablea
532    directo leyendo el `Model`.
533  - **Pendiente opcional**: re-registro de dientes al reordenar el dock (hoy se
534    registran una vez al init; el lado de activación se computa en vivo, así que el
535    drop entre lados sigue funcionando); estado "activo" del diente hospedado (hoy
536    siempre inactivo en pata, lo lleva la app).

SDD.md

SDD — pata, el marco del escritorio

Estado: Fase 8b (layer-shell sobre wlroots: barras, Quake, clicks, window_list). Este documento es la fuente autoritativa de qué es pata y dónde termina, por encima de README.

0. El problema que resuelve

El escritorio de gioser tenía el concepto de "launcher" triplicado y mal delimitado: mirada-launcher-llimphi (la barra), shuma-shell-llimphi (un chasis con tabs) y shuma-module-launcher (un módulo lista-de-apps) competían por el mismo rol sin una frontera clara. Correr cualquiera bajo el compositor daba ventanas sueltas en vez de un escritorio coherente.

pata fija la frontera: es una sola capa, la del marco (chrome) del escritorio, desacoplada del compositor y del shell, y portable entre Linux y wawa.

1. Las tres capas (no se solapan)

CapaQuechua/esQué esQué no es
miradamirarEl compositor: el Cuerpo Wayland/DRM. Tesela, acopla franjas, decora, enruta input.No dibuja barras ni widgets.
shuma—El shell: input + terminal + módulos. Se asoma como una barra inferior auto-escondible; al escribir, despliega el resto estilo Quake.No es el chrome del escritorio; es un inquilino del marco.
pataborde, repisa, andénEl marco: barras, paneles y dock declarados desde un archivo de config, con widgets colocables en cualquier slot. Hospeda el input de shuma.No compone ventanas (eso es mirada) ni ejecuta comandos (eso es shuma).

Regla mnemónica: mirada pone las ventanas, pata pone el marco alrededor, shuma es la boca por la que le hablás al sistema.

2. Forma del dominio

pata-core      Modelo agnóstico (no_std + alloc): el esquema declarativo
               (Config → [Surface] → slots → [WidgetSpec]) + el layout
               (resolve: config+pantalla → superficies colocadas + work_area).
               No pinta, no toca el SO. Cruza a wawa por `path`, como mirada-layout.
pata-config    Loader Linux (std): lee el TOML del usuario desde las rutas XDG y
               lo materializa en el modelo. El límite std→no_std del marco. Trae
               el binario `pata` para inspeccionar la geometría resuelta. En wawa
               este rol lo cumple akasha, no este crate.
pata-llimphi   Frontend Linux: monta pata-core sobre Llimphi. Cada Surface es
               una ventana Wayland que el compositor mirada acopla; despacha
               los widgets builtin; el shuma_input despliega shuma. (Fase 3,
               hereda de mirada-launcher-llimphi.)
pata (wawa)    El kernel launcher de wawa consume el MISMO pata-core y pinta
               sobre el framebuffer. (Fase 7.)

UIs intercambiables sobre un *-core agnóstico — la regla dura del repo. El modelo se escribe una vez; Linux y wawa son dos pinceles.

3. El modelo (pata-core)

El formato en disco no es parte del modelo: en Linux un loader TOML deserializa directo a estos tipos vía serde (contrato fijado en tests/toml_contract.rs); en wawa el config llega por akasha.

4. Widgets builtin previstos

start_button · window_list (ventanas abiertas, vía mirada-ctl/-link) · clipboard · volume · brightness · tray · clock · medidores (ram_meter/cpu_meter) · astro (posición zodiacal del sol + ciclo lunar, reusando cosmos-ephemeris) · shuma_input (el cabezal del shell).

Cada uno se coloca libremente: superficie + slot se eligen desde el config.

5. Integración con shuma (el Quake)

El shuma_input es un widget que vive típicamente en una Surface { kind: Bar, anchor: Bottom, autohide: true }. Muestra el cabezal del shell; al recibir foco/escritura, el frontend anima el despliegue del resto de shuma sobre el escritorio (drawer estilo Quake) y lo repliega al soltar. El marco provee el borde; shuma provee el contenido.

6. Estado y plan por fases