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> { // 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:`, `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), /// 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::().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::().unwrap(), a); + } + + #[test] + fn spawn_without_a_command_is_rejected() { + assert!("spawn:".parse::().is_err()); + assert!("spawn: ".parse::().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 { - 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: 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), /// 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: 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.