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>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user