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>
This commit is contained in:
@@ -5,6 +5,10 @@
|
|||||||
# por # se ignoran. Cada línea se pasa a `sh -c`, así que valen las
|
# 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.
|
# 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.
|
# Una terminal para empezar.
|
||||||
foot
|
foot
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, point, prelude::*, px, App, Bounds, Context, CursorStyle, Element, ElementId, FocusHandle,
|
div, point, prelude::*, px, App, Application, Bounds, Context, CursorStyle, Element, ElementId,
|
||||||
GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId, MouseButton,
|
FocusHandle, GlobalElementId, Hsla, InspectorElementId, IntoElement, KeyDownEvent, LayoutId,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render, ScrollHandle,
|
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PathBuilder, Pixels, Render,
|
||||||
SharedString, Style, Window,
|
ScrollHandle, SharedString, Style, Window, WindowBounds, WindowOptions,
|
||||||
};
|
};
|
||||||
use nahual_launcher::launch_app;
|
use nahual_launcher::launch_app;
|
||||||
use nahual_theme::Theme;
|
use nahual_theme::Theme;
|
||||||
@@ -337,6 +337,9 @@ struct Shell {
|
|||||||
scroll: ScrollHandle,
|
scroll: ScrollHandle,
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
focused_once: bool,
|
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 {
|
impl Shell {
|
||||||
@@ -378,6 +381,7 @@ impl Shell {
|
|||||||
scroll: ScrollHandle::new(),
|
scroll: ScrollHandle::new(),
|
||||||
focus: cx.focus_handle(),
|
focus: cx.focus_handle(),
|
||||||
focused_once: false,
|
focused_once: false,
|
||||||
|
launcher: false,
|
||||||
};
|
};
|
||||||
shell.start_loop(cx);
|
shell.start_loop(cx);
|
||||||
shell
|
shell
|
||||||
@@ -966,6 +970,120 @@ impl Shell {
|
|||||||
}
|
}
|
||||||
cx.notify();
|
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.
|
/// Color de resaltado de cada clase de token.
|
||||||
@@ -1247,8 +1365,12 @@ impl Render for Shell {
|
|||||||
window.focus(&self.focus);
|
window.focus(&self.focus);
|
||||||
self.focused_once = true;
|
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 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 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 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 accent = gpui::hsla(190.0 / 360.0, 0.70, 0.62, 1.0);
|
||||||
@@ -1454,7 +1576,7 @@ impl Render for Shell {
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.gap(px(8.))
|
.gap(px(8.))
|
||||||
.p(px(10.))
|
.p(px(10.))
|
||||||
.bg(bg.clone())
|
.bg(bg)
|
||||||
.when(runs_empty, |d| {
|
.when(runs_empty, |d| {
|
||||||
d.child(div().text_color(dim).child(
|
d.child(div().text_color(dim).child(
|
||||||
"Escribe un comando abajo y presiona Enter — su salida aparece aquí.",
|
"Escribe un comando abajo y presiona Enter — su salida aparece aquí.",
|
||||||
@@ -1555,49 +1677,10 @@ impl Render for Shell {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --- Zona prompt: el input inteligente ---
|
// --- 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 mut input_row: Vec<gpui::Div> = vec![div().flex_none().text_color(accent).child("› ")];
|
||||||
let cursor = self.line.cursor();
|
input_row.extend(self.input_row(&theme));
|
||||||
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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let input_bar = div()
|
let input_bar = div()
|
||||||
.h(px(46.))
|
.h(px(46.))
|
||||||
.flex()
|
.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() {
|
fn main() {
|
||||||
|
// 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);
|
launch_app("brahman · shuma shell", (1100., 700.), Shell::new);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) |
|
| 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: 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 |
|
| `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` |
|
||||||
|
|
||||||
|
`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.
|
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
|
||||||
|
|||||||
@@ -1010,6 +1010,7 @@
|
|||||||
Lanzador de apps: mirada-launcher (escanea los .desktop, lista filtrable de terminal); atado a Super+p.
|
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).
|
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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user