gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

feat(shuma): modo launcher — shuma-shell como el shell de carmen

Fase 3a del plan «shell»: `shuma-shell --launcher` (o la variable
`MIRADA_SHELL`) arranca el shell como una barra compacta acoplada al
pie de carmen, en vez del panel de 3 columnas.

- `run_launcher` abre la ventana GPUI sin barra de título y con
  `app_id = "carmen.shell"` — el acople del compositor la reconoce y le
  reserva su franja. GPUI 0.2 admite `WindowOptions.app_id`.
- `Shell.launcher: bool`; `Render::render` deriva a `render_launcher`
  cuando está activo: una barra de una línea — un glifo, la línea de
  comandos y el estado del último comando (en curso / ✓ / ✗).
- La construcción de la fila del input (tokens coloreados + caret +
  sugerencia fantasma) sale a un helper `input_row` que comparten el
  panel completo y el modo launcher — sin duplicar el resaltado.

`shuma-shell --launcher` va al `autostart.example`. Falta (3b/c/d): la
barra de ventanas abiertas, el cajón de resultados y la config.

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

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

Semántico sayk'uy · 7

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

4 qillqakuna · +179 −50

diff --git a/crates/apps/mirada-compositor/session/autostart.example b/crates/apps/mirada-compositor/session/autostart.example
index 4084be8..043bc71 100644
--- a/crates/apps/mirada-compositor/session/autostart.example
+++ b/crates/apps/mirada-compositor/session/autostart.example
@@ -5,6 +5,10 @@
 # por # se ignoran. Cada línea se pasa a `sh -c`, así que valen las
 # variables, las tuberías y el `&` final no hace falta.
 
+# El shell de carmen — barra acoplada al pie con su línea de comandos.
+# carmen la reconoce por su app_id y le reserva la franja.
+shuma-shell --launcher
+
 # Una terminal para empezar.
 foot
 
diff --git a/crates/apps/shuma-shell/src/main.rs b/crates/apps/shuma-shell/src/main.rs
index 848f75f..595d3ce 100644
--- a/crates/apps/shuma-shell/src/main.rs
+++ b/crates/apps/shuma-shell/src/main.rs
@@ -22,10 +22,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
 use std::collections::HashMap;
 
 use gpui::{
-    div, point, prelude::*, px, App, Bounds, Context, CursorStyle, Element, ElementId, FocusHandle,
-    GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId, MouseButton,
-    MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render, ScrollHandle,
-    SharedString, Style, Window,
+    div, point, prelude::*, px, App, Application, Bounds, Context, CursorStyle, Element, ElementId,
+    FocusHandle, GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId,
+    MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render,
+    ScrollHandle, SharedString, Style, Window, WindowBounds, WindowOptions,
 };
 use nahual_launcher::launch_app;
 use nahual_theme::Theme;
@@ -337,6 +337,9 @@ struct Shell {
     scroll: ScrollHandle,
     focus: FocusHandle,
     focused_once: bool,
+    /// `true` cuando el shell corre como **modo launcher**: una barra
+    /// compacta acoplada al pie de carmen, en vez del panel completo.
+    launcher: bool,
 }
 
 impl Shell {
@@ -378,6 +381,7 @@ impl Shell {
             scroll: ScrollHandle::new(),
             focus: cx.focus_handle(),
             focused_once: false,
+            launcher: false,
         };
         shell.start_loop(cx);
         shell
@@ -966,6 +970,120 @@ impl Shell {
         }
         cx.notify();
     }
+
+    /// Construye la fila del input: los tokens coloreados, el caret en su
+    /// sitio y el sufijo fantasma. Sin el prefijo del prompt — lo pone
+    /// quien la usa. La comparten el panel completo y el modo launcher.
+    fn input_row(&self, theme: &Theme) -> Vec<gpui::Div> {
+        let accent = gpui::hsla(190.0 / 360.0, 0.70, 0.62, 1.0);
+        let dim = theme.fg_muted;
+        let mut row: Vec<gpui::Div> = Vec::new();
+        let cursor = self.line.cursor();
+        let tokens = self.line.tokens();
+        let caret = || div().w(px(2.)).h(px(19.)).bg(accent);
+        if tokens.is_empty() {
+            row.push(caret());
+            row.push(
+                div()
+                    .text_color(dim)
+                    .child("escribe un comando…  (Tab autocompleta · Enter ejecuta)"),
+            );
+        } else {
+            let mut caret_done = false;
+            for t in &tokens {
+                let color = token_color(t.kind, theme);
+                if !caret_done && cursor >= t.start && cursor < t.end {
+                    let local = cursor - t.start;
+                    let (left_s, right_s) = t.text.split_at(local);
+                    if !left_s.is_empty() {
+                        row.push(div().flex_none().text_color(color).child(left_s.to_string()));
+                    }
+                    row.push(caret());
+                    row.push(div().flex_none().text_color(color).child(right_s.to_string()));
+                    caret_done = true;
+                } else {
+                    row.push(div().flex_none().text_color(color).child(t.text.clone()));
+                }
+            }
+            if !caret_done {
+                row.push(caret());
+            }
+        }
+        if let Some(ghost) = self.compute_ghost() {
+            row.push(
+                div()
+                    .flex_none()
+                    .text_color(theme.fg_disabled)
+                    .child(SharedString::from(ghost)),
+            );
+        }
+        row
+    }
+
+    /// El modo launcher: una barra compacta —glifo, input, estado del
+    /// último comando— pensada 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 accent = gpui::hsla(190.0 / 360.0, 0.70, 0.62, 1.0);
+        let dim = theme.fg_muted;
+        let text = theme.fg_text;
+
+        // 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())))
+        } else if let Some(last) = self.session.history().last() {
+            let (glyph, color) = match last.status {
+                RunStatus::Running => ("▷", accent),
+                RunStatus::Ok => ("✓", gpui::hsla(140.0 / 360.0, 0.48, 0.55, 1.0)),
+                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>());
+            }
+            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)))
+        } else {
+            div().flex_none()
+        };
+
+        div()
+            .size_full()
+            .flex()
+            .flex_row()
+            .items_center()
+            .gap(px(10.))
+            .px(px(12.))
+            .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()
+                    .flex_row()
+                    .items_center()
+                    .flex_1()
+                    .overflow_hidden()
+                    .children(self.input_row(&theme)),
+            )
+            .child(status)
+    }
 }
 
 /// Color de resaltado de cada clase de token.
@@ -1247,8 +1365,12 @@ impl Render for Shell {
             window.focus(&self.focus);
             self.focused_once = true;
         }
+        // Modo launcher: una barra compacta, no el panel de 3 columnas.
+        if self.launcher {
+            return self.render_launcher(cx);
+        }
         let theme = Theme::global(cx).clone();
-        let bg = theme.bg_app.clone();
+        let bg = theme.bg_app;
         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);
@@ -1454,7 +1576,7 @@ impl Render for Shell {
             .flex_col()
             .gap(px(8.))
             .p(px(10.))
-            .bg(bg.clone())
+            .bg(bg)
             .when(runs_empty, |d| {
                 d.child(div().text_color(dim).child(
                     "Escribe un comando abajo y presiona Enter — su salida aparece aquí.",
@@ -1555,49 +1677,10 @@ impl Render for Shell {
         };
 
         // --- Zona prompt: el input inteligente ---
+        // El prefijo `›`, y el resto (tokens + caret + fantasma) lo arma
+        // el helper compartido con el modo launcher.
         let mut input_row: Vec<gpui::Div> = vec![div().flex_none().text_color(accent).child("›  ")];
-        let cursor = self.line.cursor();
-        let tokens = self.line.tokens();
-        let caret = || div().w(px(2.)).h(px(19.)).bg(accent);
-        if tokens.is_empty() {
-            input_row.push(caret());
-            input_row.push(
-                div()
-                    .text_color(dim)
-                    .child("escribe un comando…  (Tab autocompleta · Enter ejecuta)"),
-            );
-        } else {
-            let mut caret_done = false;
-            for t in &tokens {
-                let color = token_color(t.kind, &theme);
-                if !caret_done && cursor >= t.start && cursor < t.end {
-                    let local = cursor - t.start;
-                    let (left_s, right_s) = t.text.split_at(local);
-                    if !left_s.is_empty() {
-                        input_row
-                            .push(div().flex_none().text_color(color).child(left_s.to_string()));
-                    }
-                    input_row.push(caret());
-                    input_row
-                        .push(div().flex_none().text_color(color).child(right_s.to_string()));
-                    caret_done = true;
-                } else {
-                    input_row.push(div().flex_none().text_color(color).child(t.text.clone()));
-                }
-            }
-            if !caret_done {
-                input_row.push(caret());
-            }
-        }
-        // Sugerencia fantasma — el resto que el shell predice, en gris.
-        if let Some(ghost) = self.compute_ghost() {
-            input_row.push(
-                div()
-                    .flex_none()
-                    .text_color(theme.fg_disabled)
-                    .child(SharedString::from(ghost)),
-            );
-        }
+        input_row.extend(self.input_row(&theme));
         let input_bar = div()
             .h(px(46.))
             .flex()
@@ -1778,6 +1861,41 @@ impl Drop for Shell {
     }
 }
 
+/// Levanta el shell en **modo launcher**: una ventana sin barra de
+/// título y con `app_id` `carmen.shell`, para que el compositor la
+/// reconozca y la acople a la franja del pie.
+fn run_launcher() {
+    Application::new().run(|cx: &mut App| {
+        Theme::install_default(cx);
+        let bounds = Bounds::centered(None, gpui::size(px(1280.), px(40.)), cx);
+        cx.open_window(
+            WindowOptions {
+                window_bounds: Some(WindowBounds::Windowed(bounds)),
+                titlebar: None,
+                app_id: Some("carmen.shell".into()),
+                ..Default::default()
+            },
+            |_w, cx| {
+                cx.new(|cx| {
+                    let mut shell = Shell::new(cx);
+                    shell.launcher = true;
+                    shell
+                })
+            },
+        )
+        .expect("open window");
+        cx.activate(true);
+    });
+}
+
 fn main() {
-    launch_app("brahman · shuma shell", (1100., 700.), Shell::new);
+    // Modo launcher: barra acoplada a carmen. Lo activan el argumento
+    // `--launcher` o la variable de entorno `MIRADA_SHELL`.
+    let launcher = std::env::args().any(|a| a == "--launcher")
+        || std::env::var_os("MIRADA_SHELL").is_some();
+    if launcher {
+        run_launcher();
+    } else {
+        launch_app("brahman · shuma shell", (1100., 700.), Shell::new);
+    }
 }
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 0b642e7..2c88a74 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -241,8 +241,14 @@ 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: barra + input + cajón sobre el acople shell |
+| `shuma-shell`      | modo launcher: falta la barra de ventanas y el cajón       |
 | `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.
+
 CRIU (congelar/restaurar ventanas) queda anotado como futuro.
diff --git a/vamos.txt b/vamos.txt
index 50125c1..6bd87bc 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1010,6 +1010,7 @@
   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.
   Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
   Ver crates/apps/mirada-compositor/README.md.