diff --git a/crates/apps/mirada-ctl/src/main.rs b/crates/apps/mirada-ctl/src/main.rs index 3185a1a..005b442 100644 --- a/crates/apps/mirada-ctl/src/main.rs +++ b/crates/apps/mirada-ctl/src/main.rs @@ -131,6 +131,7 @@ Acciones de mirada-ctl: promote-to-master la ventana enfocada al puesto maestro workspace activa el escritorio n (1..9) send-to-workspace manda la enfocada al escritorio n + focus-output-next pasa el foco al siguiente monitor quit apaga el compositor " ); diff --git a/crates/apps/mirada/src/main.rs b/crates/apps/mirada/src/main.rs index 71c0476..d57103b 100644 --- a/crates/apps/mirada/src/main.rs +++ b/crates/apps/mirada/src/main.rs @@ -25,6 +25,7 @@ //! j / k foco siguiente/anterior , / . nmaster −/+ //! Shift+j / k mueve la enfocada 1..9 ir a escritorio //! Enter promueve a maestra Ctrl+1..9 enviar a escritorio +//! o siguiente monitor //! ``` //! //! Los pips de escritorio y las ventanas del lienzo son **clicables**, y @@ -299,6 +300,7 @@ impl Mirada { "s" => self.act(DesktopAction::SetLayout(LayoutMode::Spiral)), "h" => self.act(DesktopAction::ShrinkMaster), "l" => self.act(DesktopAction::GrowMaster), + "o" => self.act(DesktopAction::FocusOutputNext), "enter" => self.act(DesktopAction::PromoteToMaster), "," => self.act(DesktopAction::IncMaster), "." => self.act(DesktopAction::DecMaster), diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md index 657de06..7760779 100644 --- a/crates/modules/mirada/SDD.md +++ b/crates/modules/mirada/SDD.md @@ -62,7 +62,8 @@ ejecuta operaciones de geometría". - **`mirada-brain`** — `Desktop`: salidas, 9 escritorios virtuales, registro de ventanas. `on_event(BodyEvent) -> Vec`; `DesktopAction` + `Keymap` configurable (`set_keymap` en caliente) + - `Rules` de ventana. + `Rules` de ventana. **Multi-monitor**: cada `Output` muestra un + escritorio; `relayout()` tesela todas las salidas en un solo `Place`. - **`mirada-link`** — `Link` sobre socket Unix; hilo lector de fondo + canal `mpsc` para sondeo no bloqueante. `BrainLink`/`BodyLink`, `connected_pair` (socketpair), `connect`/`listen` por ruta. @@ -122,6 +123,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres: cubre toda la salida (sin gap), oculta al resto y se lleva el foco; `Workspace.fullscreen: Option`, y el Cuerpo le fija el estado `xdg_toplevel Fullscreen`. +- **Multi-monitor** — cada `Output` muestra un escritorio distinto; + `SwitchWorkspace` actúa sobre la salida enfocada (y la intercambia si + el escritorio pedido ya lo muestra otra salida); `FocusOutputNext` + (`Super+o`) mueve el foco entre monitores. El foco del teclado es + único — sólo la ventana enfocada de la salida enfocada. - **Layout y área maestra por el API** — los 7 modos se intercambian (`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`); @@ -164,7 +170,7 @@ a las ya abiertas. ## Estado Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol` -(11), `mirada-brain` (52), `mirada-link` (7), `mirada-body` (14), las +(11), `mirada-brain` (58), `mirada-link` (7), `mirada-body` (14), las apps `mirada` y `mirada-compositor` (compilan; verificación visual manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`). diff --git a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs index 3129bf0..68fb788 100644 --- a/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs +++ b/crates/modules/mirada/mirada-brain/examples/headless-ctl.rs @@ -29,9 +29,10 @@ fn main() { }; eprintln!("Cerebro headless · control en {}", path.display()); - // Una pantalla y tres ventanas de muestra. + // Dos pantallas y tres ventanas de muestra. let mut desktop = Desktop::new(); desktop.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 }); + desktop.on_event(BodyEvent::OutputAdded { id: 1, width: 1920, height: 1080 }); for id in 1..=3 { desktop.on_event(BodyEvent::WindowOpened { id, @@ -93,10 +94,21 @@ fn main() { fn print_state(d: &Desktop) { let ws = d.active_workspace(); eprintln!( - " escritorio {} · {:?} (maestra {:.0}%) · foco {:?}", + " activo: escritorio {} · {:?} (maestra {:.0}%) · foco {:?}", d.active_index() + 1, ws.params().mode, ws.params().master_ratio * 100.0, d.focused_window(), ); + for (i, o) in d.outputs().iter().enumerate() { + let mark = if i == d.focused_output() { '*' } else { ' ' }; + eprintln!( + " {mark} salida {} {}×{} @ x{} → escritorio {}", + o.id, + o.rect.w, + o.rect.h, + o.rect.x, + o.workspace + 1, + ); + } } diff --git a/crates/modules/mirada/mirada-brain/src/action.rs b/crates/modules/mirada/mirada-brain/src/action.rs index 819b36b..fd6445e 100644 --- a/crates/modules/mirada/mirada-brain/src/action.rs +++ b/crates/modules/mirada/mirada-brain/src/action.rs @@ -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), diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs index b7d5a6d..23407a8 100644 --- a/crates/modules/mirada/mirada-brain/src/desktop.rs +++ b/crates/modules/mirada/mirada-brain/src/desktop.rs @@ -16,22 +16,34 @@ pub struct WindowInfo { pub title: String, } +/// Una salida física y el escritorio virtual que muestra ahora mismo. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Output { + pub id: OutputId, + /// Rectángulo en el espacio global — las salidas van en fila horizontal. + pub rect: Rect, + /// Índice del escritorio que esta salida muestra. + pub workspace: usize, +} + /// El estado completo del escritorio. /// /// Mantiene las salidas físicas, [`WORKSPACE_COUNT`] escritorios -/// virtuales, el registro de ventanas y el mapa de atajos. El único +/// virtuales, el registro de ventanas, el keymap y las reglas. El único /// punto de entrada es [`Desktop::on_event`]: traga un [`BodyEvent`], /// muta el estado y devuelve los [`BrainCommand`]s a enviar al Cuerpo. /// -/// Limitación de v1: el teselado se calcula sobre la salida primaria -/// (la primera conectada). El multi-monitor real llegará después. +/// **Multi-monitor**: cada salida muestra un escritorio distinto; el +/// teselado se calcula para todas y el `Place` resultante las cubre. Un +/// escritorio se ve en una salida como mucho — pedir uno que ya muestra +/// otra salida las intercambia. pub struct Desktop { /// Salidas físicas, en fila horizontal y en orden de aparición. - outputs: Vec<(OutputId, Rect)>, + outputs: Vec, /// Escritorios virtuales — `WORKSPACE_COUNT` fijos. workspaces: Vec, - /// Índice del escritorio activo. - active: usize, + /// Índice (en `outputs`) de la salida con el foco. + focused_output: usize, /// Identidad de cada ventana conocida. windows: HashMap, /// Atajos globales → acción. Configurable, recargable en caliente. @@ -62,7 +74,7 @@ impl Desktop { Self { outputs: Vec::new(), workspaces, - active: 0, + focused_output: 0, windows: HashMap::new(), keymap, rules: Rules::default(), @@ -93,9 +105,9 @@ impl Desktop { &self.keymap } - /// Geometría de la salida primaria, si hay alguna conectada. + /// Geometría de la salida enfocada, si hay alguna conectada. pub fn screen(&self) -> Option { - self.outputs.first().map(|(_, r)| *r) + self.outputs.get(self.focused_output).map(|o| o.rect) } /// Procesa un evento del Cuerpo: muta el estado y devuelve los @@ -103,13 +115,26 @@ impl Desktop { pub fn on_event(&mut self, event: BodyEvent) -> Vec { match event { BodyEvent::OutputAdded { id, width, height } => { - // Las salidas se alinean en fila a la derecha de las previas. - let x: i32 = self.outputs.iter().map(|(_, r)| r.w).sum(); - self.outputs.push((id, Rect::new(x, 0, width, height))); + // La salida nueva muestra el primer escritorio que no + // muestre ya otra salida. + let taken: Vec = self.outputs.iter().map(|o| o.workspace).collect(); + let workspace = (0..self.workspaces.len()) + .find(|n| !taken.contains(n)) + .unwrap_or(0); + self.outputs.push(Output { + id, + rect: Rect::new(0, 0, width, height), + workspace, + }); + self.reflow_outputs(); self.relayout() } BodyEvent::OutputRemoved { id } => { - self.outputs.retain(|(o, _)| *o != id); + self.outputs.retain(|o| o.id != id); + if self.focused_output >= self.outputs.len() { + self.focused_output = self.outputs.len().saturating_sub(1); + } + self.reflow_outputs(); self.relayout() } BodyEvent::WindowOpened { id, app_id, title } => { @@ -119,7 +144,7 @@ impl Desktop { let ws = outcome .workspace .filter(|&n| n < self.workspaces.len()) - .unwrap_or(self.active); + .unwrap_or(self.active_index()); self.workspaces[ws].add(id); if outcome.floating { let rect = self @@ -147,7 +172,8 @@ impl Desktop { BodyEvent::PointerEntered { id } => { // Foco al pasar el puntero, sólo si la ventana está en el // escritorio activo. - if self.workspaces[self.active].focus_window(id) { + let active = self.active_index(); + if self.workspaces[active].focus_window(id) { self.relayout() } else { Vec::new() @@ -163,51 +189,52 @@ impl Desktop { /// Aplica una acción de escritorio directamente (sin pasar por una /// tecla). Útil para disparar acciones desde un HUD. pub fn apply(&mut self, action: DesktopAction) -> Vec { + let active = self.active_index(); match action { DesktopAction::FocusNext => { - self.workspaces[self.active].focus_next(); + self.workspaces[active].focus_next(); self.relayout() } DesktopAction::FocusPrev => { - self.workspaces[self.active].focus_prev(); + self.workspaces[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) { + // está en otro, lo traemos a la salida enfocada. + if self.workspaces[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; + if n != active && self.workspaces[n].focus_window(id) { + self.show_workspace(n); return self.relayout(); } } Vec::new() } DesktopAction::MoveForward => { - self.workspaces[self.active].move_focused_forward(); + self.workspaces[active].move_focused_forward(); self.relayout() } DesktopAction::MoveBackward => { - self.workspaces[self.active].move_focused_backward(); + self.workspaces[active].move_focused_backward(); self.relayout() } DesktopAction::CloseFocused => { // Pedimos el cierre; el estado se actualiza al recibir el // `WindowClosed` de vuelta, no antes. - match self.workspaces[self.active].focused() { + match self.workspaces[active].focused() { Some(id) => vec![BrainCommand::Close(id)], None => Vec::new(), } } DesktopAction::ToggleFloat => { - let Some(id) = self.workspaces[self.active].focused() else { + let Some(id) = self.workspaces[active].focused() else { return Vec::new(); }; let screen = self.screen(); - let ws = &mut self.workspaces[self.active]; + let ws = &mut self.workspaces[active]; if ws.is_floating(id) { ws.set_floating(id, None); } else { @@ -219,10 +246,10 @@ impl Desktop { self.relayout() } DesktopAction::ToggleFullscreen => { - let Some(id) = self.workspaces[self.active].focused() else { + let Some(id) = self.workspaces[active].focused() else { return Vec::new(); }; - let ws = &mut self.workspaces[self.active]; + let ws = &mut self.workspaces[active]; if ws.fullscreen() == Some(id) { ws.set_fullscreen(None); } else { @@ -231,12 +258,12 @@ impl Desktop { self.relayout() } DesktopAction::CycleLayout => { - let next = self.workspaces[self.active].params().mode.next(); - self.workspaces[self.active].set_mode(next); + let next = self.workspaces[active].params().mode.next(); + self.workspaces[active].set_mode(next); self.relayout() } DesktopAction::SetLayout(mode) => { - self.workspaces[self.active].set_mode(mode); + self.workspaces[active].set_mode(mode); self.relayout() } DesktopAction::GrowMaster => self.nudge_master(0.05), @@ -244,38 +271,83 @@ impl Desktop { DesktopAction::IncMaster => self.nudge_master_count(1), DesktopAction::DecMaster => self.nudge_master_count(-1), DesktopAction::PromoteToMaster => { - self.workspaces[self.active].promote_focused(); + self.workspaces[active].promote_focused(); self.relayout() } DesktopAction::SwitchWorkspace(n) => { - if n < self.workspaces.len() && n != self.active { - self.active = n; + if n < self.workspaces.len() && n != active { + self.show_workspace(n); self.relayout() } else { Vec::new() } } DesktopAction::SendToWorkspace(n) => { - if n >= self.workspaces.len() || n == self.active { + if n >= self.workspaces.len() || n == active { return Vec::new(); } - match self.workspaces[self.active].focused() { + match self.workspaces[active].focused() { Some(id) => { - self.workspaces[self.active].remove(id); + self.workspaces[active].remove(id); self.workspaces[n].add(id); self.relayout() } None => Vec::new(), } } + DesktopAction::FocusOutputNext => { + if self.outputs.len() > 1 { + self.focused_output = (self.focused_output + 1) % self.outputs.len(); + self.relayout() + } else { + Vec::new() + } + } DesktopAction::Quit => vec![BrainCommand::Shutdown], } } + /// El índice del escritorio activo — el que muestra la salida + /// enfocada. `0` si todavía no hay ninguna salida. + pub fn active_index(&self) -> usize { + self.outputs + .get(self.focused_output) + .map(|o| o.workspace) + .unwrap_or(0) + } + + /// Hace que la salida enfocada muestre el escritorio `n`. Si otra + /// salida ya lo mostraba, intercambian — así ningún escritorio se + /// ve en dos sitios a la vez. + fn show_workspace(&mut self, n: usize) { + if n >= self.workspaces.len() || self.focused_output >= self.outputs.len() { + return; + } + let current = self.outputs[self.focused_output].workspace; + if current == n { + return; + } + if let Some(other) = self.outputs.iter().position(|o| o.workspace == n) { + self.outputs[other].workspace = current; + } + self.outputs[self.focused_output].workspace = n; + } + + /// Recoloca las salidas en fila horizontal, en su orden de aparición. + fn reflow_outputs(&mut self) { + let mut x = 0; + for o in &mut self.outputs { + o.rect.x = x; + o.rect.y = 0; + x += o.rect.w; + } + } + /// Ajusta la fracción del área maestra del escritorio activo (la usan /// `MasterStack` y `CenteredMaster`), acotada a `0.05..=0.95`. fn nudge_master(&mut self, delta: f32) -> Vec { - let ws = &mut self.workspaces[self.active]; + let active = self.active_index(); + let ws = &mut self.workspaces[active]; let ratio = (ws.params().master_ratio + delta).clamp(0.05, 0.95); ws.set_master_ratio(ratio); self.relayout() @@ -283,52 +355,61 @@ impl Desktop { /// Ajusta `nmaster` del escritorio activo, acotado a `1..=9`. fn nudge_master_count(&mut self, delta: i32) -> Vec { - let ws = &mut self.workspaces[self.active]; + let active = self.active_index(); + let ws = &mut self.workspaces[active]; let n = (ws.params().master_count as i32 + delta).clamp(1, 9) as usize; ws.set_master_count(n); self.relayout() } - /// Recalcula la geometría del escritorio activo y la empaqueta en un - /// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que + /// Recalcula la geometría de **todas** las salidas y la empaqueta en + /// un único [`BrainCommand::Place`]. Sin salidas, no hay nada que /// colocar. fn relayout(&self) -> Vec { - match self.screen() { - Some(screen) => { - vec![BrainCommand::Place(placements( - &self.workspaces[self.active], - screen, - ))] - } - None => Vec::new(), + if self.outputs.is_empty() { + return Vec::new(); } + let mut all = Vec::new(); + for o in &self.outputs { + all.extend(placements(&self.workspaces[o.workspace], o.rect)); + } + // El foco del teclado es único: sólo la ventana enfocada de la + // salida enfocada. `placements` marca el foco por escritorio (lo + // necesita para la visibilidad en `Monocle`); aquí lo unificamos. + let global_focus = self.focused_window(); + for p in &mut all { + p.focused = Some(p.id) == global_focus; + } + vec![BrainCommand::Place(all)] } // --- Accesores de sólo lectura, para el HUD de la app GPUI --------- - /// Índice del escritorio activo. - pub fn active_index(&self) -> usize { - self.active - } - - /// El escritorio activo. + /// El escritorio activo — el de la salida enfocada. pub fn active_workspace(&self) -> &Workspace { - &self.workspaces[self.active] + &self.workspaces[self.active_index()] } - /// Las salidas conectadas, en orden. - pub fn outputs(&self) -> &[(OutputId, Rect)] { + /// Las salidas conectadas, en orden, con el escritorio que muestran. + pub fn outputs(&self) -> &[Output] { &self.outputs } + /// Índice (en [`outputs`](Desktop::outputs)) de la salida enfocada. + pub fn focused_output(&self) -> usize { + self.focused_output + } + /// Identidad de una ventana conocida. pub fn window_info(&self, id: WindowId) -> Option<&WindowInfo> { self.windows.get(&id) } - /// La ventana enfocada en el escritorio activo. + /// La ventana con el foco del teclado: la enfocada del escritorio + /// activo — o su ventana en pantalla completa, si la hay. pub fn focused_window(&self) -> Option { - self.workspaces[self.active].focused() + let ws = &self.workspaces[self.active_index()]; + ws.fullscreen().or_else(|| ws.focused()) } /// Cuántas ventanas hay en cada escritorio virtual. @@ -339,6 +420,7 @@ impl Desktop { /// 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 { + let active = self.active_index(); let mut lines = Vec::new(); for (n, ws) in self.workspaces.iter().enumerate() { let ws_focus = ws.focused(); @@ -349,7 +431,7 @@ impl Desktop { 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), + focused: n == active && ws_focus == Some(id), }); } } @@ -709,6 +791,16 @@ mod tests { ); } + // --- Multi-monitor ------------------------------------------------- + + /// Un escritorio con dos salidas 1920×1080. + fn desktop_with_two_outputs() -> Desktop { + let mut d = Desktop::new(); + d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 }); + d.on_event(BodyEvent::OutputAdded { id: 1, width: 1920, height: 1080 }); + d + } + #[test] fn outputs_lay_side_by_side() { let mut d = Desktop::new(); @@ -716,8 +808,69 @@ mod tests { d.on_event(BodyEvent::OutputAdded { id: 1, width: 2560, height: 1440 }); assert_eq!(d.outputs().len(), 2); // La segunda salida arranca donde acaba la primera. - assert_eq!(d.outputs()[1].1.x, 1920); - // El teselado sigue sobre la salida primaria. - assert_eq!(d.screen().unwrap().w, 1920); + assert_eq!(d.outputs()[1].rect.x, 1920); + } + + #[test] + fn each_output_shows_a_distinct_workspace() { + let d = desktop_with_two_outputs(); + assert_eq!(d.outputs()[0].workspace, 0); + assert_eq!(d.outputs()[1].workspace, 1); + } + + #[test] + fn switching_to_a_workspace_shown_on_another_output_swaps_them() { + let mut d = desktop_with_two_outputs(); + // La salida enfocada (0, ws 0) pide el ws 1, que muestra la 1 → swap. + d.apply(DesktopAction::SwitchWorkspace(1)); + assert_eq!(d.outputs()[0].workspace, 1); + assert_eq!(d.outputs()[1].workspace, 0); + } + + #[test] + fn focus_output_next_moves_the_focus_between_outputs() { + let mut d = desktop_with_two_outputs(); + assert_eq!(d.active_index(), 0); // salida 0 → ws 0 + d.apply(DesktopAction::FocusOutputNext); + assert_eq!(d.active_index(), 1); // salida 1 → ws 1 + d.apply(DesktopAction::FocusOutputNext); // envuelve + assert_eq!(d.active_index(), 0); + } + + #[test] + fn relayout_places_windows_on_every_output() { + let mut d = Desktop::new(); + d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 }); + d.on_event(BodyEvent::OutputAdded { id: 1, width: 1280, height: 720 }); + open(&mut d, 1); // en la salida 0 (ws 0) + d.apply(DesktopAction::FocusOutputNext); + let cmds = open(&mut d, 2); // en la salida 1 (ws 1) + let p = places(&cmds); + assert_eq!(p.len(), 2); + // Cada ventana cae en el rectángulo de su salida. + assert_eq!(p.iter().find(|x| x.id == 1).unwrap().rect.x, 0); + assert_eq!(p.iter().find(|x| x.id == 2).unwrap().rect.x, 1920); + } + + #[test] + fn keyboard_focus_is_unique_across_outputs() { + let mut d = desktop_with_two_outputs(); + open(&mut d, 1); + d.apply(DesktopAction::FocusOutputNext); + let cmds = open(&mut d, 2); + // Sólo una ventana con foco de teclado en todo el Place. + assert_eq!(places(&cmds).iter().filter(|p| p.focused).count(), 1); + } + + #[test] + fn removing_an_output_keeps_its_windows_in_their_workspace() { + let mut d = desktop_with_two_outputs(); + d.apply(DesktopAction::FocusOutputNext); // foco en la salida 1 (ws 1) + open(&mut d, 1); // en ws 1 + d.on_event(BodyEvent::OutputRemoved { id: 1 }); + // La ventana sigue registrada, en el ws 1. + assert!(d.window_info(1).is_some()); + assert_eq!(d.workspace_loads()[1], 1); + assert_eq!(d.outputs().len(), 1); } } diff --git a/crates/modules/mirada/mirada-brain/src/keymap.rs b/crates/modules/mirada/mirada-brain/src/keymap.rs index f59985e..8318cb2 100644 --- a/crates/modules/mirada/mirada-brain/src/keymap.rs +++ b/crates/modules/mirada/mirada-brain/src/keymap.rs @@ -245,6 +245,7 @@ const KEYMAP_HEADER: &str = "\ // grow-master / shrink-master redimensiona el área maestra // inc-master / dec-master nº de ventanas maestras (nmaster) // promote-to-master la enfocada al puesto maestro +// 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 // quit apaga el compositor diff --git a/crates/modules/mirada/mirada-brain/src/lib.rs b/crates/modules/mirada/mirada-brain/src/lib.rs index f467ea8..8dfa252 100644 --- a/crates/modules/mirada/mirada-brain/src/lib.rs +++ b/crates/modules/mirada/mirada-brain/src/lib.rs @@ -26,7 +26,7 @@ pub mod rules; pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT}; pub use ctl::{CtlConn, CtlReply, CtlRequest, CtlServer, WindowLine}; -pub use desktop::{Desktop, WindowInfo}; +pub use desktop::{Desktop, Output, WindowInfo}; pub use keymap::{Keymap, KeymapError, KeymapWatch}; pub use rules::{Rule, RuleOutcome, Rules}; diff --git a/vamos.txt b/vamos.txt index 5f02e12..8a240bb 100644 --- a/vamos.txt +++ b/vamos.txt @@ -1073,5 +1073,14 @@ + Multi-monitor real — el Desktop tesela todas las salidas: + Cada Output muestra un escritorio distinto; relayout() las tesela todas en un solo Place. + Pedir un escritorio que ya muestra otra salida → las intercambia (ningún escritorio se ve 2 veces). + mirada-ctl focus-output-next # Super+o — mueve el foco al siguiente monitor + El foco del teclado es único: sólo la ventana enfocada de la salida enfocada. + cargo run -p mirada-brain --example headless-ctl # ahora levanta 2 salidas + + +