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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user