feat(mirada): API de acciones — mirada-ctl + HUD interactivo

Toda acción de escritorio converge en Desktop::apply(DesktopAction); el
keymap era sólo un front-end. Esta tanda añade los otros tres.

- DesktopAction::FocusWindow(WindowId): direccionamiento directo de una
  ventana (no sólo ciclar); si está en otro escritorio, salta a él.
  DesktopAction pasa a ser Serialize/Deserialize (postcard) además de
  Display/FromStr.

- mirada-brain::ctl: el API de control externo. CtlRequest/CtlReply
  (marco postcard), CtlServer/CtlConn no bloqueantes y send_request.
  El Cerebro abre el socket y atiende en su bucle: la app mirada
  siempre, mirada-compositor sólo con el Cerebro embebido.

- mirada-ctl: CLI de control estilo swaymsg/hyprctl —
  `mirada-ctl focus-next | focus-window 5 | workspace 3 | windows`.
  Parsea la acción de los argumentos vía FromStr.

- HUD interactivo en la app mirada: pips de escritorio y ventanas del
  lienzo clicables (SwitchWorkspace / FocusWindow).

- Ejemplo headless-ctl: un Cerebro sin gráficos para probar mirada-ctl
  en modo desatendido. Verificado end-to-end.

mirada-brain: 29 -> 37 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 00:20:10 +00:00
parent 8204852e3a
commit b31f988833
14 changed files with 751 additions and 12 deletions
@@ -149,6 +149,20 @@ impl Desktop {
self.workspaces[self.active].focus_prev();
self.relayout()
}
DesktopAction::FocusWindow(id) => {
// En el escritorio activo basta enfocar; si la ventana
// está en otro, saltamos a ese escritorio.
if self.workspaces[self.active].focus_window(id) {
return self.relayout();
}
for n in 0..self.workspaces.len() {
if n != self.active && self.workspaces[n].focus_window(id) {
self.active = n;
return self.relayout();
}
}
Vec::new()
}
DesktopAction::MoveForward => {
self.workspaces[self.active].move_focused_forward();
self.relayout()
@@ -245,6 +259,26 @@ impl Desktop {
pub fn workspace_loads(&self) -> Vec<usize> {
self.workspaces.iter().map(Workspace::len).collect()
}
/// Una vista de todas las ventanas conocidas, en todos los
/// escritorios — la base de `mirada-ctl windows` y de una taskbar.
pub fn window_lines(&self) -> Vec<crate::ctl::WindowLine> {
let mut lines = Vec::new();
for (n, ws) in self.workspaces.iter().enumerate() {
let ws_focus = ws.focused();
for &id in ws.windows() {
let info = self.windows.get(&id);
lines.push(crate::ctl::WindowLine {
id,
app_id: info.map(|i| i.app_id.clone()).unwrap_or_default(),
title: info.map(|i| i.title.clone()).unwrap_or_default(),
workspace: n + 1,
focused: n == self.active && ws_focus == Some(id),
});
}
}
lines
}
}
/// El siguiente modo en el ciclo de [`DesktopAction::CycleLayout`].
@@ -317,6 +351,48 @@ mod tests {
assert!(d.on_event(BodyEvent::Keybind("Super+j".into())).is_empty());
}
#[test]
fn focus_window_addresses_a_specific_window() {
let mut d = desktop_with_screen();
for id in [1, 2, 3] {
open(&mut d, id);
}
assert_eq!(d.focused_window(), Some(3));
d.apply(DesktopAction::FocusWindow(1));
assert_eq!(d.focused_window(), Some(1));
}
#[test]
fn focus_window_jumps_to_the_workspace_that_holds_it() {
let mut d = desktop_with_screen();
open(&mut d, 1);
open(&mut d, 2); // enfocada
// Manda la 2 al escritorio 3; seguimos en el 1.
d.on_event(BodyEvent::Keybind("Super+Shift+3".into()));
assert_eq!(d.active_index(), 0);
// Enfocar la 2 nos lleva a su escritorio.
d.apply(DesktopAction::FocusWindow(2));
assert_eq!(d.active_index(), 2);
assert_eq!(d.focused_window(), Some(2));
}
#[test]
fn window_lines_cover_every_window_with_its_workspace() {
let mut d = desktop_with_screen();
open(&mut d, 1);
open(&mut d, 2);
d.on_event(BodyEvent::Keybind("Super+Shift+3".into())); // la 2 al esc. 3
let lines = d.window_lines();
assert_eq!(lines.len(), 2);
let w1 = lines.iter().find(|l| l.id == 1).unwrap();
let w2 = lines.iter().find(|l| l.id == 2).unwrap();
assert_eq!(w1.workspace, 1);
assert_eq!(w2.workspace, 3);
// La 1 quedó enfocada en el escritorio activo (el 1).
assert!(w1.focused);
assert!(!w2.focused);
}
#[test]
fn without_a_screen_nothing_is_placed() {
let mut d = Desktop::new();