feat(mirada): multi-monitor real — cada salida tesela su escritorio

El Desktop deja de teselar sólo la salida primaria. Cada Output muestra
un escritorio virtual distinto y relayout() las tesela todas en un solo
Place que cubre todas las pantallas.

- Output { id, rect, workspace }; focused_output reemplaza al índice
  global active. active_index() = el escritorio de la salida enfocada.
- OutputAdded asigna el primer escritorio libre; OutputRemoved deja sus
  ventanas en su escritorio y reajusta el foco. reflow_outputs() las
  recoloca en fila.
- SwitchWorkspace actúa sobre la salida enfocada; si el escritorio
  pedido ya lo muestra otra salida, las intercambia (invariante: un
  escritorio se ve en una salida como mucho).
- DesktopAction::FocusOutputNext (Super+o) mueve el foco entre
  monitores. El foco del teclado es único — relayout() lo unifica a la
  ventana enfocada de la salida enfocada.

Verificado end-to-end con headless-ctl (ahora 2 salidas).
mirada-brain 52->58 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 01:18:12 +00:00
parent be61ddb6eb
commit 799dcef22e
9 changed files with 258 additions and 69 deletions
@@ -60,6 +60,8 @@ pub enum DesktopAction {
SwitchWorkspace(usize),
/// Manda la ventana enfocada al escritorio virtual `n`.
SendToWorkspace(usize),
/// Mueve el foco a la siguiente salida (monitor).
FocusOutputNext,
/// Apaga el compositor.
Quit,
}
@@ -114,6 +116,7 @@ impl fmt::Display for DesktopAction {
// Los escritorios se numeran 1-based de cara al usuario.
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::Quit => f.write_str("quit"),
}
}
@@ -139,6 +142,7 @@ impl FromStr for DesktopAction {
"inc-master" => Self::IncMaster,
"dec-master" => Self::DecMaster,
"promote-to-master" => Self::PromoteToMaster,
"focus-output-next" => Self::FocusOutputNext,
"quit" => Self::Quit,
_ => {
if let Some(slug) = s.strip_prefix("layout:") {
@@ -203,6 +207,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
("Super+s".into(), DesktopAction::SetLayout(LayoutMode::Spiral)),
("Super+h".into(), DesktopAction::ShrinkMaster),
("Super+l".into(), DesktopAction::GrowMaster),
("Super+o".into(), DesktopAction::FocusOutputNext),
("Super+Return".into(), DesktopAction::PromoteToMaster),
("Super+,".into(), DesktopAction::IncMaster),
("Super+.".into(), DesktopAction::DecMaster),