From 2bd6aaad0249e0a87d5a5c55ea1a250b5f258eac Mon Sep 17 00:00:00 2001 From: sergio Date: Thu, 21 May 2026 07:05:14 +0000 Subject: [PATCH] =?UTF-8?q?feat(shuma):=20caj=C3=B3n=20de=20resultados=20d?= =?UTF-8?q?el=20shell=20=E2=80=94=20desplegable=20desde=20el=20pie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../apps/mirada-compositor/src/drm_backend.rs | 10 +- crates/apps/mirada-compositor/src/main.rs | 18 ++- crates/apps/shuma-shell/src/main.rs | 117 +++++++++++++----- crates/modules/mirada/SDD.md | 10 +- vamos.txt | 1 + 5 files changed, 115 insertions(+), 41 deletions(-) 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::::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> { // (í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> { 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, + /// `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::()); + if line.chars().count() > 30 { + line = format!("{}…", line.chars().take(30).collect::()); } - 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.