tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

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>

a9e880240dbe2aeae515bed368c5f97b3d049132 · ruwaq sergio · (5 killa ñawpaq) · tayta b17ac8c67 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

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

5 qillqakuna · +96 −6

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.