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>
This commit is contained in:
sergio
2026-05-21 07:05:14 +00:00
parent a9e880240d
commit 2bd6aaad02
5 changed files with 115 additions and 41 deletions
@@ -152,6 +152,7 @@ impl DrmState {
if self.pending_flip { if self.pending_flip {
return; // aún esperamos el VBlank del cuadro anterior 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 // 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 // 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 { if !w.visible || w.is_shell {
continue; // el shell no lleva marco 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 (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL }; let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
let rects = border_rects(x, y, sw, sh); 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(); let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
shown.sort_by_key(|w| (!w.is_shell, !w.floating)); shown.sort_by_key(|w| (!w.is_shell, !w.floating));
for w in &shown { 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); let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
// El marco, encima de la propia superficie de la ventana // El marco, encima de la propia superficie de la ventana
// — el shell no lleva. // — el shell no lleva.
@@ -507,7 +508,7 @@ impl DrmState {
let hit = self.window_at(x, y); let hit = self.window_at(x, y);
let focus = hit.map(|i| { let focus = hit.map(|i| {
let w = &self.app.windows[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(), w.surface.clone(),
Point::<f64, Logical>::from((lx as f64, ly as f64)), Point::<f64, Logical>::from((lx as f64, ly as f64)),
@@ -593,9 +594,10 @@ impl DrmState {
let w = &self.app.windows[i]; let w = &self.app.windows[i];
(!w.is_shell, !w.floating) (!w.is_shell, !w.floating)
}); });
let output_h = self.app.output_size.1;
idx.into_iter().find(|&i| { idx.into_iter().find(|&i| {
let w = &self.app.windows[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); 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 x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
}) })
+12 -6
View File
@@ -676,11 +676,16 @@ fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
// Bucle principal // Bucle principal
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/// Dónde pintar una ventana: su celda asignada, pero si el cliente /// Dónde pintar una ventana. La del shell se ancla al pie de la salida
/// presenta una superficie más pequeña que la celda (p. ej. un terminal /// y crece hacia arriba (su cajón de resultados se despliega sobre las
/// que redondea su tamaño a celdas de texto enteras), se centra en el /// ventanas). Una ventana normal va en su celda; si el cliente presenta
/// hueco sobrante en vez de dejarlo todo a un lado. /// una superficie más pequeña que la celda (p. ej. un terminal que
fn render_loc(w: &ManagedWindow) -> (i32, i32) { /// 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()) { match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
Some(Some(size)) => { Some(Some(size)) => {
let dx = ((w.size.0 - size.w) / 2).max(0); 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 // (índice 0 = encima): el shell primero —va sobre todo—, luego
// las flotantes, luego las teseladas. `sort_by_key` es estable: // las flotantes, luego las teseladas. `sort_by_key` es estable:
// dentro de cada grupo se respeta el orden de apertura. // dentro de cada grupo se respeta el orden de apertura.
let output_h = state.output_size.1;
let mut shown: Vec<&ManagedWindow> = let mut shown: Vec<&ManagedWindow> =
state.windows.iter().filter(|w| w.visible).collect(); state.windows.iter().filter(|w| w.visible).collect();
shown.sort_by_key(|w| (!w.is_shell, !w.floating)); 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( render_elements_from_surface_tree(
renderer, renderer,
&w.surface, &w.surface,
render_loc(w), render_loc(w, output_h),
1.0, 1.0,
1.0, 1.0,
Kind::Unspecified, Kind::Unspecified,
+83 -22
View File
@@ -38,6 +38,10 @@ use shuma_sysmon::{Snapshot, SystemSampler};
/// Cuántas muestras guarda la curva de cada monitor. /// Cuántas muestras guarda la curva de cada monitor.
const HISTORY: usize = 80; 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. /// Archivos/directorios que delatan la estructura de un proyecto.
const PROJECT_MARKERS: &[&str] = &[ const PROJECT_MARKERS: &[&str] = &[
".git", ".git",
@@ -362,6 +366,9 @@ struct Shell {
/// Las ventanas abiertas del escritorio, según el socket de control /// Las ventanas abiertas del escritorio, según el socket de control
/// de carmen — la barra de tareas del modo launcher. /// de carmen — la barra de tareas del modo launcher.
windows_bar: Vec<WindowLine>, 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 { impl Shell {
@@ -405,6 +412,7 @@ impl Shell {
focused_once: false, focused_once: false,
launcher: false, launcher: false,
windows_bar: Vec::new(), windows_bar: Vec::new(),
drawer_open: false,
}; };
shell.start_loop(cx); shell.start_loop(cx);
shell shell
@@ -1110,13 +1118,10 @@ impl Shell {
.overflow_hidden() .overflow_hidden()
.children(chips); .children(chips);
// Estado a la derecha: nº de comandos en curso, o el último. // Estado a la derecha: nº en curso, o el último comando. Un clic
let status = if !self.active.is_empty() { // despliega o repliega el cajón de resultados.
div() let (status_text, status_color) = if !self.active.is_empty() {
.flex_none() (format!("{} en curso", self.active.len()), accent)
.text_size(px(12.))
.text_color(accent)
.child(SharedString::from(format!("{} en curso", self.active.len())))
} else if let Some(last) = self.session.history().last() { } else if let Some(last) = self.session.history().last() {
let (glyph, color) = match last.status { let (glyph, color) = match last.status {
RunStatus::Running => ("", accent), RunStatus::Running => ("", accent),
@@ -1124,36 +1129,51 @@ impl Shell {
RunStatus::Failed => ("", gpui::hsla(2.0 / 360.0, 0.68, 0.60, 1.0)), RunStatus::Failed => ("", gpui::hsla(2.0 / 360.0, 0.68, 0.60, 1.0)),
}; };
let mut line = last.line.clone(); let mut line = last.line.clone();
if line.chars().count() > 32 { if line.chars().count() > 30 {
line = format!("{}", line.chars().take(32).collect::<String>()); line = format!("{}", line.chars().take(30).collect::<String>());
} }
div() (format!("{glyph} {line}"), color)
} else {
("sin comandos".to_string(), dim)
};
let caret = if self.drawer_open { "" } else { "" };
let status = div()
.id("drawer-toggle")
.flex_none() .flex_none()
.flex() .flex()
.flex_row() .flex_row()
.items_center() .items_center()
.gap(px(5.)) .gap(px(5.))
.px(px(6.))
.rounded(px(4.))
.text_size(px(12.)) .text_size(px(12.))
.child(div().text_color(color).child(glyph)) .cursor_pointer()
.child(div().text_color(dim).child(SharedString::from(line))) .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 { } else {
div().flex_none() LAUNCHER_BAR_H
}; };
window.resize(gpui::size(w, px(h)));
cx.notify();
}));
div() // La barra propiamente dicha — glifo, input, ventanas, estado.
.size_full() let bar = div()
.h(px(LAUNCHER_BAR_H))
.flex_none()
.flex() .flex()
.flex_row() .flex_row()
.items_center() .items_center()
.gap(px(10.)) .gap(px(10.))
.px(px(12.)) .px(px(12.))
.overflow_hidden() .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().flex_none().text_color(accent).child(""))
.child( .child(
div() div()
@@ -1165,7 +1185,48 @@ impl Shell {
.children(self.input_row(&theme)), .children(self.input_row(&theme)),
) )
.child(taskbar) .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)
} }
} }
+7 -3
View File
@@ -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) | | 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 | | `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 | | `wlr-layer-shell` | barras externas tipo waybar, fondos, notificaciones |
| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` | | `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 reconoce) y dibuja una barra compacta — glifo, la línea de comandos de
`shuma-line`, la **barra de ventanas abiertas** (las consulta por el `shuma-line`, la **barra de ventanas abiertas** (las consulta por el
socket de control de carmen con `ListWindows`, un clic enfoca con socket de control de carmen con `ListWindows`, un clic enfoca con
`FocusWindow`) y el estado del último comando. Falta el cajón de `FocusWindow`) y el estado del último comando. Un clic en el estado
resultados expandible y la config para esconder/reubicar. **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. CRIU (congelar/restaurar ventanas) queda anotado como futuro.
+1
View File
@@ -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. 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. 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. 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. Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
Ver crates/apps/mirada-compositor/README.md. Ver crates/apps/mirada-compositor/README.md.