feat(shuma): barra de ventanas en el modo launcher
Fase 3b: la barra del shell muestra ahora las ventanas abiertas del
escritorio y deja saltar entre ellas.
- `shuma-shell` depende de `mirada-brain` para hablar el protocolo de
control de carmen.
- `start_loop` sondea el socket de control cada ~1 s con `ListWindows`
— la llamada bloquea un instante, pero en el executor de fondo, no en
el hilo de la UI. El resultado se guarda en `Shell.windows_bar`.
- `render_launcher` dibuja una cajita por ventana entre el input y el
estado: la enfocada resaltada, las demás en gris. Un clic envía
`Do(FocusWindow(id))` y refleja el cambio al instante (el sondeo lo
confirma en el siguiente ciclo).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
diff --git a/Cargo.lock b/Cargo.lock
index 897097f..256ff4b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12163,6 +12163,7 @@ dependencies = [
"matilda-core",
"matilda-discover",
"matilda-plan",
+ "mirada-brain",
"nahual-launcher",
"nahual-theme",
"serde_json",
diff --git a/crates/apps/shuma-shell/Cargo.toml b/crates/apps/shuma-shell/Cargo.toml
index 62e4488..eb478eb 100644
--- a/crates/apps/shuma-shell/Cargo.toml
+++ b/crates/apps/shuma-shell/Cargo.toml
@@ -18,6 +18,9 @@ shuma-session = { path = "../../modules/shuma/shuma-session" }
shuma-exec = { path = "../../modules/shuma/shuma-exec" }
shuma-infer = { path = "../../modules/shuma/shuma-infer" }
shuma-sysmon = { path = "../../modules/shuma/shuma-sysmon" }
+# El protocolo de control de carmen — el modo launcher lista y enfoca
+# ventanas a través de él.
+mirada-brain = { path = "../../modules/mirada/mirada-brain" }
# Herramienta matilda, embebida en la ventana del shell.
matilda-core = { path = "../../modules/matilda/matilda-core" }
matilda-plan = { path = "../../modules/matilda/matilda-plan" }
diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs
index 595d3ce..f4f5bb3 100644
--- a/crates/apps/shuma-shell/src/main.rs
+++ b/crates/apps/shuma-shell/src/main.rs
@@ -27,6 +27,8 @@ use gpui::{
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render,
ScrollHandle, SharedString, Style, Window, WindowBounds, WindowOptions,
};
+use mirada_brain::ctl::{default_socket_path, send_request};
+use mirada_brain::{CtlReply, CtlRequest, DesktopAction, WindowLine};
use nahual_launcher::launch_app;
use nahual_theme::Theme;
use shuma_exec::{run as exec_run, CommandSpec, Exec, RunEvent, RunHandle, StageSpec};
@@ -69,6 +71,23 @@ fn fkey_index(key: &str) -> Option<usize> {
(1..=8).contains(&n).then_some(n - 1)
}
+/// Pregunta a carmen, por su socket de control, la lista de ventanas
+/// abiertas. `None` si el compositor no está o respondió otra cosa.
+fn poll_ctl_windows() -> Option<Vec<WindowLine>> {
+ match send_request(&default_socket_path(), &CtlRequest::ListWindows) {
+ Ok(CtlReply::Windows(w)) => Some(w),
+ _ => None,
+ }
+}
+
+/// Pide a carmen que enfoque una ventana del escritorio.
+fn focus_window(id: u64) {
+ let _ = send_request(
+ &default_socket_path(),
+ &CtlRequest::Do(DesktopAction::FocusWindow(id)),
+ );
+}
+
/// Quita las comillas exteriores de un argumento (`"hola"` → `hola`).
fn unquote(arg: &str) -> String {
let b = arg.as_bytes();
@@ -340,6 +359,9 @@ struct Shell {
/// `true` cuando el shell corre como **modo launcher**: una barra
/// compacta acoplada al pie de carmen, en vez del panel completo.
launcher: bool,
+ /// Las ventanas abiertas del escritorio, según el socket de control
+ /// de carmen — la barra de tareas del modo launcher.
+ windows_bar: Vec<WindowLine>,
}
impl Shell {
@@ -382,6 +404,7 @@ impl Shell {
focus: cx.focus_handle(),
focused_once: false,
launcher: false,
+ windows_bar: Vec::new(),
};
shell.start_loop(cx);
shell
@@ -396,12 +419,22 @@ impl Shell {
cx.background_executor().timer(Duration::from_millis(110)).await;
tick += 1;
let sysmon = tick % 10 == 0;
+ // Cada ~1 s pregunta a carmen por sus ventanas. La llamada
+ // bloquea un instante sobre un socket Unix local — aquí,
+ // en el executor de fondo, no en el hilo de la UI.
+ let windows = (tick % 9 == 0).then(poll_ctl_windows).flatten();
let alive = this.update(cx, |shell, cx| {
let mut changed = shell.drain_exec();
if sysmon {
shell.snapshot = shell.sampler.sample();
changed = true;
}
+ if let Some(w) = windows {
+ if w != shell.windows_bar {
+ shell.windows_bar = w;
+ changed = true;
+ }
+ }
if changed {
cx.notify();
}
@@ -1020,15 +1053,63 @@ impl Shell {
row
}
- /// El modo launcher: una barra compacta —glifo, input, estado del
- /// último comando— pensada para la franja que carmen reserva al pie.
+ /// El modo launcher: una barra compacta —glifo, input, barra de
+ /// ventanas, estado del último comando— para la franja que carmen
+ /// reserva al pie.
fn render_launcher(&mut self, cx: &mut Context<Self>) -> gpui::Div {
let theme = Theme::global(cx).clone();
let panel = gpui::hsla(220.0 / 360.0, 0.16, 0.11, 1.0);
+ let node_bg = gpui::hsla(220.0 / 360.0, 0.14, 0.16, 1.0);
let accent = gpui::hsla(190.0 / 360.0, 0.70, 0.62, 1.0);
let dim = theme.fg_muted;
let text = theme.fg_text;
+ // Barra de tareas: una cajita por ventana abierta, la enfocada
+ // resaltada. Un clic se la pide a carmen por el socket de control.
+ let chips: Vec<_> = self
+ .windows_bar
+ .iter()
+ .map(|w| {
+ let id = w.id;
+ let raw = if !w.title.is_empty() { &w.title } else { &w.app_id };
+ let label = if raw.chars().count() > 18 {
+ format!("{}…", raw.chars().take(18).collect::<String>())
+ } else {
+ raw.clone()
+ };
+ let focused = w.focused;
+ div()
+ .id(SharedString::from(format!("win-{id}")))
+ .flex_none()
+ .px(px(8.))
+ .py(px(3.))
+ .rounded(px(4.))
+ .text_size(px(12.))
+ .cursor_pointer()
+ .when(focused, |d| {
+ d.bg(accent).text_color(gpui::hsla(0.0, 0.0, 0.12, 1.0))
+ })
+ .when(!focused, |d| d.bg(node_bg).text_color(dim))
+ .child(SharedString::from(label))
+ .on_click(cx.listener(move |shell, _, _, cx| {
+ focus_window(id);
+ // Eco inmediato — el sondeo confirma en ~1 s.
+ for w in &mut shell.windows_bar {
+ w.focused = w.id == id;
+ }
+ cx.notify();
+ }))
+ })
+ .collect();
+ let taskbar = div()
+ .flex()
+ .flex_row()
+ .items_center()
+ .gap(px(4.))
+ .flex_none()
+ .overflow_hidden()
+ .children(chips);
+
// Estado a la derecha: nº de comandos en curso, o el último.
let status = if !self.active.is_empty() {
div()
@@ -1066,6 +1147,7 @@ impl Shell {
.items_center()
.gap(px(10.))
.px(px(12.))
+ .overflow_hidden()
.bg(panel)
.text_color(text)
.text_size(px(13.))
@@ -1082,6 +1164,7 @@ impl Shell {
.overflow_hidden()
.children(self.input_row(&theme)),
)
+ .child(taskbar)
.child(status)
}
}
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 2c88a74..abf2d76 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -241,14 +241,16 @@ 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 la barra de ventanas y el cajón |
+| `shuma-shell` | modo launcher: falta el cajón de resultados y la config |
| `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.
+`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.
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
diff --git a/vamos.txt b/vamos.txt
index 6bd87bc..42aa566 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1010,7 +1010,8 @@
Lanzador de apps: mirada-launcher (escanea los .desktop, lista filtrable de terminal); atado a Super+p.
Conmutación de VT: Ctrl+Alt+Fn salta a otra TTY y vuelve sin romper la sesión (pausa DRM + libinput).
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 de shuma-line.
+ 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.
Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
Ver crates/apps/mirada-compositor/README.md.