gioser

tawasuyu/brahman

Rust 521TOML 202Markdown 57JSON 197505 Definiciones

feat(mirada): acción spawn — lanzar programas desde el compositor

Un escritorio sin forma de abrir una terminal no es usable. Ahora el
keymap puede lanzar programas:

- `mirada-protocol`: nuevo `BrainCommand::Spawn(String)`.
- `mirada-brain`: `DesktopAction::Spawn(String)` con forma textual
  `spawn:<comando>` (`Display`/`FromStr`); `Desktop::apply` la traduce
  a `BrainCommand::Spawn`. El keymap por defecto trae
  `Super+Shift+Return` → `spawn:foot`. `DesktopAction` deja de ser
  `Copy` (lleva el comando) — `Keymap::lookup` clona en vez de copiar.
- `mirada-body`: `BodyOp::Spawn(String)`.
- `mirada-compositor`: `exec_op` ejecuta el spawn con un helper
  `spawn_command` (`sh -c`, hereda `WAYLAND_DISPLAY`), que también
  recoge el lanzamiento de `MIRADA_STARTUP` — antes duplicado.

`spawn:foot --title x` también funciona desde `mirada-ctl`. Tests
nuevos del round-trip textual y del flujo atajo→comando.

Nota: un keymap.ron ya existente no recibe el atajo nuevo; hay que
añadir la línea a mano o borrar el archivo para regenerarlo.

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

fb3091d9957028deb77e7ceb94c2ea2db54508a0 · por sergio · (hace 5 meses) · padre 90bffec3f · ver el árbol en este commit

Delta semántico · 17

crates/apps/mirada-compositor/src/drm_backend.rs

  • cambió por dentro fn run pub

crates/apps/mirada-compositor/src/main.rs

crates/modules/mirada/mirada-body/src/lib.rs

  • cambió de firma enum BodyOp pub
  • cambió por dentro fn apply pub

crates/modules/mirada/mirada-brain/src/action.rs

crates/modules/mirada/mirada-brain/src/desktop.rs

crates/modules/mirada/mirada-brain/src/keymap.rs

crates/modules/mirada/mirada-protocol/src/lib.rs

10 archivos · +76 −16

diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md
index f515bb6..5091fe1 100644
--- a/crates/apps/mirada-compositor/README.md
+++ b/crates/apps/mirada-compositor/README.md
@@ -78,11 +78,12 @@ WAYLAND_DISPLAY=wayland-1 foot      # o weston-terminal, alacritty, …
 ```
 
 Las ventanas se teselan solas. El teclado, con la ventana del compositor
-enfocada, maneja el escritorio con atajos `Super+…`: foco `Super+j/k`,
-los 7 layouts en `Super+t/m/g/c/r/d/s` (o ciclar con `Super+space`), área
-maestra `Super+h/l`, `nmaster` `Super+,/.`, promover a maestra
-`Super+Return`, escritorios `Super+1..9`, cerrar `Super+q`. Cierra la
-ventana del compositor para salir.
+enfocada, maneja el escritorio con atajos `Super+…`: lanzar una terminal
+`Super+Shift+Return`, foco `Super+j/k`, los 7 layouts en
+`Super+t/m/g/c/r/d/s` (o ciclar con `Super+space`), área maestra
+`Super+h/l`, `nmaster` `Super+,/.`, promover a maestra `Super+Return`,
+escritorios `Super+1..9`, cerrar `Super+q`. Cierra la ventana del
+compositor para salir.
 
 ## Atajos de teclado
 
diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index 2cd538c..2c6c964 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -609,12 +609,7 @@ pub fn run() -> Result<(), Box<dyn Error>> {
     // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
     // hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
     if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
-        if !cmd.trim().is_empty() {
-            match std::process::Command::new("sh").arg("-c").arg(&cmd).spawn() {
-                Ok(child) => println!("      app de arranque lanzada (pid {}): {cmd}", child.id()),
-                Err(e) => eprintln!("      no pude lanzar «{cmd}»: {e}"),
-            }
-        }
+        crate::spawn_command(&cmd);
     }
 
     // 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index 9eaa2ff..4525152 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -262,6 +262,7 @@ impl App {
             }
             BodyOp::SetGrabs(keys) => self.grabs = keys,
             BodyOp::SetCursor(_) => {}
+            BodyOp::Spawn(cmd) => spawn_command(&cmd),
             BodyOp::Shutdown => self.running = false,
         }
     }
@@ -596,6 +597,21 @@ fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
         .map(|s| (s.w, s.h))
 }
 
+/// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el
+/// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se
+/// conecta a este compositor. Lo usan la acción `spawn:…` del keymap y
+/// la variable `MIRADA_STARTUP`.
+fn spawn_command(cmd: &str) {
+    let cmd = cmd.trim();
+    if cmd.is_empty() {
+        return;
+    }
+    match std::process::Command::new("sh").arg("-c").arg(cmd).spawn() {
+        Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
+        Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
+    }
+}
+
 /// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
 fn load_user_rules() -> Rules {
     match Rules::default_path() {
diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md
index 0c910b2..f853b6b 100644
--- a/crates/modules/mirada/SDD.md
+++ b/crates/modules/mirada/SDD.md
@@ -142,6 +142,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
   `inc`/`dec-master` cambian `nmaster` (`Super+,`/`Super+.`); y
   `promote-to-master` lleva la enfocada al puesto maestro (`Super+Return`
   — `combo_string` ya canoniza teclas con nombre: `Return`, `Tab`, `F5`…).
+- **Lanzar programas** — `DesktopAction::Spawn(String)` (forma textual
+  `spawn:<comando>`, `Super+Shift+Return` → `spawn:foot` por defecto)
+  produce un `BrainCommand::Spawn`; el Cuerpo lo ejecuta con `sh -c`, y
+  el hijo hereda `WAYLAND_DISPLAY`. `DesktopAction` deja de ser `Copy`
+  por llevar el comando.
 - **HUD interactivo** (app `mirada`) — los pips de escritorio y las
   ventanas del lienzo son clicables: clic = `apply` de la acción.
 - **`mirada-ctl`** — control externo por línea de comandos
diff --git a/crates/modules/mirada/mirada-body/src/lib.rs b/crates/modules/mirada/mirada-body/src/lib.rs
index 8d06577..001d559 100644
--- a/crates/modules/mirada/mirada-body/src/lib.rs
+++ b/crates/modules/mirada/mirada-body/src/lib.rs
@@ -74,6 +74,8 @@ pub enum BodyOp {
     SetGrabs(Vec<String>),
     /// Cambia el cursor del puntero.
     SetCursor(String),
+    /// Lanza un programa como proceso hijo del compositor.
+    Spawn(String),
     /// Apaga el compositor y libera el hardware.
     Shutdown,
 }
@@ -163,6 +165,7 @@ impl BodyState {
             BrainCommand::Kill(id) => vec![BodyOp::KillClient(id)],
             BrainCommand::GrabKeys(keys) => vec![BodyOp::SetGrabs(keys)],
             BrainCommand::SetCursor(name) => vec![BodyOp::SetCursor(name)],
+            BrainCommand::Spawn(cmd) => vec![BodyOp::Spawn(cmd)],
             BrainCommand::Shutdown => vec![BodyOp::Shutdown],
         }
     }
diff --git a/crates/modules/mirada/mirada-brain/src/action.rs b/crates/modules/mirada/mirada-brain/src/action.rs
index a59b40f..857b570 100644
--- a/crates/modules/mirada/mirada-brain/src/action.rs
+++ b/crates/modules/mirada/mirada-brain/src/action.rs
@@ -23,7 +23,9 @@ pub const WORKSPACE_COUNT: usize = 9;
 /// Es serializable (`postcard`) para viajar por el API de control
 /// ([`crate::ctl`]) y tiene una forma textual estable ([`Display`] /
 /// [`FromStr`]) para el keymap y `mirada-ctl`.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+///
+/// No es `Copy`: [`Spawn`](DesktopAction::Spawn) lleva su comando.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub enum DesktopAction {
     /// Mueve el foco a la ventana siguiente del escritorio activo.
     FocusNext,
@@ -66,6 +68,9 @@ pub enum DesktopAction {
     SendToWorkspace(usize),
     /// Mueve el foco a la siguiente salida (monitor).
     FocusOutputNext,
+    /// Lanza un programa — abre una terminal, un navegador, lo que sea.
+    /// El comando se pasa a `sh -c` en el Cuerpo.
+    Spawn(String),
     /// Apaga el compositor.
     Quit,
 }
@@ -123,6 +128,7 @@ impl fmt::Display for DesktopAction {
             DesktopAction::SwitchWorkspace(n) => write!(f, "workspace:{}", n + 1),
             DesktopAction::SendToWorkspace(n) => write!(f, "send-to-workspace:{}", n + 1),
             DesktopAction::FocusOutputNext => f.write_str("focus-output-next"),
+            DesktopAction::Spawn(cmd) => write!(f, "spawn:{cmd}"),
             DesktopAction::Quit => f.write_str("quit"),
         }
     }
@@ -168,6 +174,12 @@ impl FromStr for DesktopAction {
                     Self::SendToWorkspace(parse_workspace(n)?)
                 } else if let Some(n) = s.strip_prefix("workspace:") {
                     Self::SwitchWorkspace(parse_workspace(n)?)
+                } else if let Some(cmd) = s.strip_prefix("spawn:") {
+                    let cmd = cmd.trim();
+                    if cmd.is_empty() {
+                        return Err("spawn: necesita un comando".into());
+                    }
+                    Self::Spawn(cmd.to_string())
                 } else {
                     return Err(format!("acción desconocida: '{s}'"));
                 }
@@ -218,6 +230,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
         ("Super+l".into(), DesktopAction::GrowMaster),
         ("Super+o".into(), DesktopAction::FocusOutputNext),
         ("Super+Return".into(), DesktopAction::PromoteToMaster),
+        ("Super+Shift+Return".into(), DesktopAction::Spawn("foot".into())),
         ("Super+,".into(), DesktopAction::IncMaster),
         ("Super+.".into(), DesktopAction::DecMaster),
         ("Super+Shift+e".into(), DesktopAction::Quit),
@@ -254,10 +267,10 @@ mod tests {
         for n in 0..WORKSPACE_COUNT {
             assert!(map
                 .iter()
-                .any(|(_, a)| *a == DesktopAction::SwitchWorkspace(n)));
+                .any(|(_, a)| a == &DesktopAction::SwitchWorkspace(n)));
             assert!(map
                 .iter()
-                .any(|(_, a)| *a == DesktopAction::SendToWorkspace(n)));
+                .any(|(_, a)| a == &DesktopAction::SendToWorkspace(n)));
         }
     }
 
@@ -306,4 +319,17 @@ mod tests {
         assert_eq!(a.to_string(), "focus-window:42");
         assert_eq!("focus-window:42".parse::<DesktopAction>().unwrap(), a);
     }
+
+    #[test]
+    fn spawn_round_trips_keeping_the_whole_command() {
+        let a = DesktopAction::Spawn("foot --title diario".into());
+        assert_eq!(a.to_string(), "spawn:foot --title diario");
+        assert_eq!(a.to_string().parse::<DesktopAction>().unwrap(), a);
+    }
+
+    #[test]
+    fn spawn_without_a_command_is_rejected() {
+        assert!("spawn:".parse::<DesktopAction>().is_err());
+        assert!("spawn:   ".parse::<DesktopAction>().is_err());
+    }
 }
diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs
index b5193b0..cdeb3ec 100644
--- a/crates/modules/mirada/mirada-brain/src/desktop.rs
+++ b/crates/modules/mirada/mirada-brain/src/desktop.rs
@@ -387,6 +387,7 @@ impl Desktop {
                     Vec::new()
                 }
             }
+            DesktopAction::Spawn(cmd) => vec![BrainCommand::Spawn(cmd)],
             DesktopAction::Quit => vec![BrainCommand::Shutdown],
         }
     }
@@ -876,6 +877,13 @@ mod tests {
         assert_eq!(p.rect, target);
     }
 
+    #[test]
+    fn a_spawn_keybind_becomes_a_spawn_command() {
+        let mut d = desktop_with_screen();
+        let cmds = d.on_event(BodyEvent::Keybind("Super+Shift+Return".into()));
+        assert_eq!(cmds, vec![BrainCommand::Spawn("foot".into())]);
+    }
+
     #[test]
     fn dragging_an_unknown_window_does_nothing() {
         let mut d = desktop_with_screen();
diff --git a/crates/modules/mirada/mirada-brain/src/keymap.rs b/crates/modules/mirada/mirada-brain/src/keymap.rs
index 897cc0d..8c68bfb 100644
--- a/crates/modules/mirada/mirada-brain/src/keymap.rs
+++ b/crates/modules/mirada/mirada-brain/src/keymap.rs
@@ -67,7 +67,7 @@ impl Keymap {
 
     /// La acción asociada a una combinación, si la hay.
     pub fn lookup(&self, combo: &str) -> Option<DesktopAction> {
-        self.bindings.get(combo).copied()
+        self.bindings.get(combo).cloned()
     }
 
     /// Las combinaciones a interceptar — el contenido de un `GrabKeys`.
@@ -250,6 +250,7 @@ const KEYMAP_HEADER: &str = "\
 //   focus-output-next                pasa el foco al siguiente monitor
 //   workspace:N                      activa el escritorio N (1..9)
 //   send-to-workspace:N              manda la enfocada al escritorio N
+//   spawn:<comando>                  lanza un programa (p. ej. spawn:foot)
 //   quit                             apaga el compositor
 //
 // Edita y guarda: mirada recarga el keymap en caliente, sin reiniciar.";
diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs
index 775468c..aa87cde 100644
--- a/crates/modules/mirada/mirada-protocol/src/lib.rs
+++ b/crates/modules/mirada/mirada-protocol/src/lib.rs
@@ -67,6 +67,10 @@ pub enum BrainCommand {
     GrabKeys(Vec<String>),
     /// Cambia el cursor del puntero al nombre dado (tema XCursor).
     SetCursor(String),
+    /// Lanza un programa como proceso hijo del Cuerpo — hereda su
+    /// entorno, `WAYLAND_DISPLAY` incluido, así el cliente se conecta
+    /// aquí. La cadena se pasa a `sh -c`.
+    Spawn(String),
     /// Apaga el Cuerpo y libera el hardware.
     Shutdown,
 }
diff --git a/vamos.txt b/vamos.txt
index bf617f6..df19b11 100644
--- a/vamos.txt
+++ b/vamos.txt
@@ -1004,6 +1004,7 @@
   En --drm el ratón pinta un cursor de software, el foco sigue al puntero y clics/rueda van a la ventana debajo.
   Super+arrastre mueve la ventana (botón izq.) o la redimensiona (der.) — al arrastrarla pasa a flotar.
   Fuerza xdg-decoration ServerSide y no dibuja marco: las ventanas teseladas van sin barra de título.
+  Lanzar programas: acción spawn:<comando> del keymap (Super+Shift+Return → spawn:foot por defecto).
   Ver crates/apps/mirada-compositor/README.md.
 
 
@@ -1013,7 +1014,7 @@
   Disco: RON de texto en ~/.config/mirada/keymap.ron — editable a mano, versionable, recargado en caliente (notify).
   Si falta, la app escribe uno por defecto documentado; si está corrupto, avisa y usa el de por defecto sin pisarlo.
   cargo run -p mirada-brain --example keymap-default               # imprime el keymap por defecto en RON
-  Acciones como cadena estable: "focus-next", "layout:grid", "workspace:3" (DesktopAction: Display + FromStr).
+  Acciones como cadena estable: "focus-next", "layout:grid", "workspace:3", "spawn:foot" (DesktopAction: Display + FromStr).
   Sin ejecutable configurador: el editor de texto del usuario, y la app mirada sobre el mismo API Keymap.