gioser

tawasuyu/brahman

Rust 570TOML 215Markdown 68JSON 198261 Definiciones

feat(shuma): cajón de resultados del shell — desplegable desde el pie

Fase 3c: el shell muestra la salida de los comandos en un cajón que se
despliega hacia arriba sobre el escritorio.

carmen — la ventana del shell deja de tener un alto fijo: `render_loc`
la ancla al pie de la salida y la coloca por su **tamaño real**, así
puede crecer hacia arriba. La franja reservada sigue siendo la barra
(40 px); el cajón, al abrirse, se solapa sobre las teseladas sin
re-teselar. `render_loc` toma ahora el alto de la salida.

shuma-shell — un clic en el estado alterna `drawer_open`: la ventana
crece (`Window::resize`, que GPUI 0.2 expone) a barra + cajón, o
vuelve a sólo barra. El cajón reusa `render_run` para pintar los
últimos comandos y su salida, con scroll. `render_launcher` pasa a una
columna: cajón opcional arriba, barra abajo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

2bd6aaad0249e0a87d5a5c55ea1a250b5f258eac · por sergio · (hace 5 meses) · padre a9e880240 · ver el árbol en este commit

Delta semántico · 10

crates/apps/mirada-compositor/src/drm_backend.rs

crates/apps/mirada-compositor/src/main.rs

crates/apps/shuma-shell/src/main.rs

5 archivos · +115 −41

diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index 34f0933..4953de9 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -152,6 +152,7 @@ impl DrmState {
         if self.pending_flip {
             return; // aún esperamos el VBlank del cuadro anterior
         }
+        let output_h = self.app.output_size.1;
 
         // Paso 1 · refresca los búferes del marco de cada ventana — su
         // tamaño (sigue al contenido) y su color (según el foco). Cada
@@ -160,7 +161,7 @@ impl DrmState {
             if !w.visible || w.is_shell {
                 continue; // el shell no lleva marco
             }
-            let (x, y) = crate::render_loc(w);
+            let (x, y) = crate::render_loc(w, output_h);
             let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
             let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
             let rects = border_rects(x, y, sw, sh);
@@ -216,7 +217,7 @@ impl DrmState {
             let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
             shown.sort_by_key(|w| (!w.is_shell, !w.floating));
             for w in &shown {
-                let (x, y) = crate::render_loc(w);
+                let (x, y) = crate::render_loc(w, output_h);
                 let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
                 // El marco, encima de la propia superficie de la ventana
                 // — el shell no lleva.
@@ -507,7 +508,7 @@ impl DrmState {
         let hit = self.window_at(x, y);
         let focus = hit.map(|i| {
             let w = &self.app.windows[i];
-            let (lx, ly) = crate::render_loc(w);
+            let (lx, ly) = crate::render_loc(w, self.app.output_size.1);
             (
                 w.surface.clone(),
                 Point::<f64, Logical>::from((lx as f64, ly as f64)),
@@ -593,9 +594,10 @@ impl DrmState {
             let w = &self.app.windows[i];
             (!w.is_shell, !w.floating)
         });
+        let output_h = self.app.output_size.1;
         idx.into_iter().find(|&i| {
             let w = &self.app.windows[i];
-            let (lx, ly) = crate::render_loc(w);
+            let (lx, ly) = crate::render_loc(w, output_h);
             let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
             x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
         })
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index 04032dc..b313563 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -676,11 +676,16 @@ fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
 // Bucle principal
 // ---------------------------------------------------------------------
 
-/// Dónde pintar una ventana: su celda asignada, pero si el cliente
-/// presenta una superficie más pequeña que la celda (p. ej. un terminal
-/// que redondea su tamaño a celdas de texto enteras), se centra en el
-/// hueco sobrante en vez de dejarlo todo a un lado.
-fn render_loc(w: &ManagedWindow) -> (i32, i32) {
+/// Dónde pintar una ventana. La del shell se ancla al pie de la salida
+/// y crece hacia arriba (su cajón de resultados se despliega sobre las
+/// ventanas). Una ventana normal va en su celda; si el cliente presenta
+/// una superficie más pequeña que la celda (p. ej. un terminal que
+/// redondea su tamaño a celdas de texto), se centra en el hueco.
+fn render_loc(w: &ManagedWindow, output_h: i32) -> (i32, i32) {
+    if w.is_shell {
+        let h = surface_px_size(w).map(|(_, h)| h).unwrap_or(SHELL_DOCK_HEIGHT);
+        return (0, output_h - h);
+    }
     match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
         Some(Some(size)) => {
             let dx = ((w.size.0 - size.w) / 2).max(0);
@@ -1082,6 +1087,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
             // (índice 0 = encima): el shell primero —va sobre todo—, luego
             // las flotantes, luego las teseladas. `sort_by_key` es estable:
             // dentro de cada grupo se respeta el orden de apertura.
+            let output_h = state.output_size.1;
             let mut shown: Vec<&ManagedWindow> =
                 state.windows.iter().filter(|w| w.visible).collect();
             shown.sort_by_key(|w| (!w.is_shell, !w.floating));
@@ -1091,7 +1097,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
                     render_elements_from_surface_tree(
                         renderer,
                         &w.surface,
-                        render_loc(w),
+                        render_loc(w, output_h),
                         1.0,
                         1.0,
                         Kind::Unspecified,
diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs
index f4f5bb3..2a50831 100644
--- a/crates/apps/shuma-shell/src/main.rs
+++ b/crates/apps/shuma-shell/src/main.rs
@@ -38,6 +38,10 @@ use shuma_sysmon::{Snapshot, SystemSampler};
 
 /// Cuántas muestras guarda la curva de cada monitor.
 const HISTORY: usize = 80;
+/// Alto de la barra del modo launcher, en píxeles.
+const LAUNCHER_BAR_H: f32 = 40.0;
+/// Alto del cajón de resultados del modo launcher cuando se despliega.
+const LAUNCHER_DRAWER_H: f32 = 320.0;
 /// Archivos/directorios que delatan la estructura de un proyecto.
 const PROJECT_MARKERS: &[&str] = &[
     ".git",
@@ -362,6 +366,9 @@ struct Shell {
     /// Las ventanas abiertas del escritorio, según el socket de control
     /// de carmen — la barra de tareas del modo launcher.
     windows_bar: Vec<WindowLine>,
+    /// `true` cuando el cajón de resultados del modo launcher está
+    /// desplegado (la ventana crece hacia arriba sobre el escritorio).
+    drawer_open: bool,
 }
 
 impl Shell {
@@ -405,6 +412,7 @@ impl Shell {
             focused_once: false,
             launcher: false,
             windows_bar: Vec::new(),
+            drawer_open: false,
         };
         shell.start_loop(cx);
         shell
@@ -1110,13 +1118,10 @@ impl Shell {
             .overflow_hidden()
             .children(chips);
 
-        // Estado a la derecha: nº de comandos en curso, o el último.
-        let status = if !self.active.is_empty() {
-            div()
-                .flex_none()
-                .text_size(px(12.))
-                .text_color(accent)
-                .child(SharedString::from(format!("▷ {} en curso", self.active.len())))
+        // Estado a la derecha: nº en curso, o el último comando. Un clic
+        // despliega o repliega el cajón de resultados.
+        let (status_text, status_color) = if !self.active.is_empty() {
+            (format!("▷ {} en curso", self.active.len()), accent)
         } else if let Some(last) = self.session.history().last() {
             let (glyph, color) = match last.status {
                 RunStatus::Running => ("▷", accent),
@@ -1124,36 +1129,51 @@ impl Shell {
                 RunStatus::Failed => ("✗", gpui::hsla(2.0 / 360.0, 0.68, 0.60, 1.0)),
             };
             let mut line = last.line.clone();
-            if line.chars().count() > 32 {
-                line = format!("{}…", line.chars().take(32).collect::<String>());
+            if line.chars().count() > 30 {
+                line = format!("{}…", line.chars().take(30).collect::<String>());
             }
-            div()
-                .flex_none()
-                .flex()
-                .flex_row()
-                .items_center()
-                .gap(px(5.))
-                .text_size(px(12.))
-                .child(div().text_color(color).child(glyph))
-                .child(div().text_color(dim).child(SharedString::from(line)))
+            (format!("{glyph} {line}"), color)
         } else {
-            div().flex_none()
+            ("sin comandos".to_string(), dim)
         };
+        let caret = if self.drawer_open { "▾" } else { "▴" };
+        let status = div()
+            .id("drawer-toggle")
+            .flex_none()
+            .flex()
+            .flex_row()
+            .items_center()
+            .gap(px(5.))
+            .px(px(6.))
+            .rounded(px(4.))
+            .text_size(px(12.))
+            .cursor_pointer()
+            .hover(|s| s.bg(node_bg))
+            .child(div().text_color(dim).child(caret))
+            .child(div().text_color(status_color).child(SharedString::from(status_text)))
+            .on_click(cx.listener(|shell, _, window, cx| {
+                shell.drawer_open = !shell.drawer_open;
+                // La ventana crece o se encoge; carmen la ancla al pie.
+                let w = window.bounds().size.width;
+                let h = if shell.drawer_open {
+                    LAUNCHER_BAR_H + LAUNCHER_DRAWER_H
+                } else {
+                    LAUNCHER_BAR_H
+                };
+                window.resize(gpui::size(w, px(h)));
+                cx.notify();
+            }));
 
-        div()
-            .size_full()
+        // La barra propiamente dicha — glifo, input, ventanas, estado.
+        let bar = div()
+            .h(px(LAUNCHER_BAR_H))
+            .flex_none()
             .flex()
             .flex_row()
             .items_center()
             .gap(px(10.))
             .px(px(12.))
             .overflow_hidden()
-            .bg(panel)
-            .text_color(text)
-            .text_size(px(13.))
-            .track_focus(&self.focus)
-            .key_context("ShumaShell")
-            .on_key_down(cx.listener(Self::handle_key))
             .child(div().flex_none().text_color(accent).child("⟫"))
             .child(
                 div()
@@ -1165,7 +1185,48 @@ impl Shell {
                     .children(self.input_row(&theme)),
             )
             .child(taskbar)
-            .child(status)
+            .child(status);
+
+        // El cajón de resultados — los últimos comandos y su salida.
+        let drawer = self.drawer_open.then(|| {
+            let hist = self.session.history();
+            let start = hist.len().saturating_sub(8);
+            let runs: Vec<_> = hist[start..]
+                .iter()
+                .map(|r| {
+                    let ui = self.run_ui.get(&r.id).copied().unwrap_or_default();
+                    render_run(r, ui, &theme, node_bg, cx)
+                })
+                .collect();
+            let empty = runs.is_empty();
+            div()
+                .id("launcher-drawer")
+                .flex_1()
+                .overflow_y_scroll()
+                .track_scroll(&self.scroll)
+                .flex()
+                .flex_col()
+                .gap(px(6.))
+                .p(px(8.))
+                .bg(theme.bg_app)
+                .when(empty, |d| {
+                    d.child(div().text_color(dim).child("sin comandos todavía"))
+                })
+                .children(runs)
+        });
+
+        div()
+            .size_full()
+            .flex()
+            .flex_col()
+            .bg(panel)
+            .text_color(text)
+            .text_size(px(13.))
+            .track_focus(&self.focus)
+            .key_context("ShumaShell")
+            .on_key_down(cx.listener(Self::handle_key))
+            .children(drawer)
+            .child(bar)
     }
 }
 
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index abf2d76..51b3fe6 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -241,7 +241,7 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
 | ------------------ | ---------------------------------------------------------- |
 | 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 el cajón de resultados y la config    |
+| `shuma-shell`      | modo launcher: falta la config para esconder/reubicar      |
 | `wlr-layer-shell`  | barras externas tipo waybar, fondos, notificaciones        |
 | `mirada-sandbox`   | aislamiento de clientes sobre `arje-incarnate`             |
 
@@ -250,7 +250,11 @@ 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`, la **barra de ventanas abiertas** (las consulta por el
 socket de control de carmen con `ListWindows`, un clic enfoca con
-`FocusWindow`) y el estado del último comando. Falta el cajón de
-resultados expandible y la config para esconder/reubicar.
+`FocusWindow`) y el estado del último comando. Un clic en el estado
+**despliega el cajón de resultados**: la ventana del shell crece hacia
+arriba (`Window::resize`) y carmen la ancla al pie — `render_loc`
+coloca la ventana-shell por su tamaño real, así su cajón se solapa
+sobre las teseladas sin re-teselar. Falta la config para esconder y
+reubicar la barra.
 
 CRIU (congelar/restaurar ventanas) queda anotado como futuro.
diff --git a/vamos.txt b/vamos.txt
index 42aa566..3310b33 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1012,6 +1012,7 @@
   Acople del shell: una ventana con app_id "carmen.shell" se ancla en una franja al pie; el resto tesela arriba.
   shuma-shell --launcher: corre como ese shell — barra compacta GPUI con la línea de comandos + barra de ventanas.
   La barra de ventanas las consulta por el socket de control de carmen (ListWindows); un clic enfoca.
+  Clic en el estado despliega el cajón de resultados: la ventana del shell crece y carmen la ancla al pie.
   Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
   Ver crates/apps/mirada-compositor/README.md.