diff --git a/crates/apps/mirada-ctl/src/main.rs b/crates/apps/mirada-ctl/src/main.rs index 005b442..b4b404a 100644 --- a/crates/apps/mirada-ctl/src/main.rs +++ b/crates/apps/mirada-ctl/src/main.rs @@ -84,10 +84,13 @@ fn print_windows(windows: &[WindowLine]) { } for w in windows { let mark = if w.focused { '*' } else { ' ' }; - println!( - "{mark} id {:<4} esc {} {:<24} {}", - w.id, w.workspace, w.app_id, w.title - ); + // El escritorio 0 es el scratchpad (ventana guardada). + let ws = if w.workspace == 0 { + "scratch".to_string() + } else { + w.workspace.to_string() + }; + println!("{mark} id {:<4} esc {:<7} {:<24} {}", w.id, ws, w.app_id, w.title); } } @@ -122,6 +125,8 @@ Acciones de mirada-ctl: close-focused cierra la ventana enfocada toggle-float alterna flotante / teselada la enfocada toggle-fullscreen alterna pantalla completa en la enfocada + send-to-scratchpad guarda la ventana enfocada en el scratchpad + toggle-scratchpad invoca u oculta la ventana del scratchpad cycle-layout pasa al siguiente modo de teselado layout master-stack · centered-master · spiral grid · columns · rows · monocle diff --git a/crates/apps/mirada/src/main.rs b/crates/apps/mirada/src/main.rs index d57103b..90def87 100644 --- a/crates/apps/mirada/src/main.rs +++ b/crates/apps/mirada/src/main.rs @@ -25,7 +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 +//! o siguiente monitor ` / Shift+` scratchpad ver/guardar //! ``` //! //! Los pips de escritorio y las ventanas del lienzo son **clicables**, y @@ -301,6 +301,8 @@ impl Mirada { "h" => self.act(DesktopAction::ShrinkMaster), "l" => self.act(DesktopAction::GrowMaster), "o" => self.act(DesktopAction::FocusOutputNext), + "`" if shift => self.act(DesktopAction::SendToScratchpad), + "`" => self.act(DesktopAction::ToggleScratchpad), "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 7760779..9f1b3bc 100644 --- a/crates/modules/mirada/SDD.md +++ b/crates/modules/mirada/SDD.md @@ -128,6 +128,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres: 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. +- **Scratchpad** — `SendToScratchpad` guarda la ventana enfocada (sale + del teselado, en ningún escritorio); `ToggleScratchpad` (`` Super+` ``) + la invoca flotando y centrada en el escritorio actual, o la oculta — + estilo terminal desplegable. `Desktop.scratchpad: Vec`; + `mirada-ctl windows` la lista como `esc scratch`. - **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`); @@ -170,7 +175,7 @@ a las ya abiertas. ## Estado Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol` -(11), `mirada-brain` (58), `mirada-link` (7), `mirada-body` (14), las +(11), `mirada-brain` (63), `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/src/action.rs b/crates/modules/mirada/mirada-brain/src/action.rs index fd6445e..a59b40f 100644 --- a/crates/modules/mirada/mirada-brain/src/action.rs +++ b/crates/modules/mirada/mirada-brain/src/action.rs @@ -42,6 +42,10 @@ pub enum DesktopAction { ToggleFloat, /// Alterna pantalla completa en la ventana enfocada. ToggleFullscreen, + /// Guarda la ventana enfocada en el scratchpad (la oculta). + SendToScratchpad, + /// Invoca u oculta la ventana del scratchpad — aparece flotando. + ToggleScratchpad, /// Pasa al siguiente modo de teselado. CycleLayout, /// Fija un modo de teselado concreto. @@ -106,6 +110,8 @@ impl fmt::Display for DesktopAction { DesktopAction::CloseFocused => f.write_str("close-focused"), DesktopAction::ToggleFloat => f.write_str("toggle-float"), DesktopAction::ToggleFullscreen => f.write_str("toggle-fullscreen"), + DesktopAction::SendToScratchpad => f.write_str("send-to-scratchpad"), + DesktopAction::ToggleScratchpad => f.write_str("toggle-scratchpad"), DesktopAction::CycleLayout => f.write_str("cycle-layout"), DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)), DesktopAction::GrowMaster => f.write_str("grow-master"), @@ -136,6 +142,8 @@ impl FromStr for DesktopAction { "close-focused" => Self::CloseFocused, "toggle-float" => Self::ToggleFloat, "toggle-fullscreen" => Self::ToggleFullscreen, + "send-to-scratchpad" => Self::SendToScratchpad, + "toggle-scratchpad" => Self::ToggleScratchpad, "cycle-layout" => Self::CycleLayout, "grow-master" => Self::GrowMaster, "shrink-master" => Self::ShrinkMaster, @@ -197,6 +205,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> { ("Super+q".into(), DesktopAction::CloseFocused), ("Super+f".into(), DesktopAction::ToggleFloat), ("Super+Shift+f".into(), DesktopAction::ToggleFullscreen), + ("Super+`".into(), DesktopAction::ToggleScratchpad), ("Super+space".into(), DesktopAction::CycleLayout), ("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)), ("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)), diff --git a/crates/modules/mirada/mirada-brain/src/ctl.rs b/crates/modules/mirada/mirada-brain/src/ctl.rs index 7fa1aca..bd77d12 100644 --- a/crates/modules/mirada/mirada-brain/src/ctl.rs +++ b/crates/modules/mirada/mirada-brain/src/ctl.rs @@ -52,7 +52,8 @@ pub struct WindowLine { pub id: WindowId, pub app_id: String, pub title: String, - /// Escritorio virtual donde está (1-based). + /// Escritorio virtual donde está (1-based); `0` = guardada en el + /// scratchpad, en ningún escritorio. pub workspace: usize, /// `true` si es la ventana enfocada del escritorio activo. pub focused: bool, diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs index 23407a8..7c31699 100644 --- a/crates/modules/mirada/mirada-brain/src/desktop.rs +++ b/crates/modules/mirada/mirada-brain/src/desktop.rs @@ -50,6 +50,9 @@ pub struct Desktop { keymap: Keymap, /// Reglas de ventana — escritorio/flotante por `app_id`/título. rules: Rules, + /// Ventanas del scratchpad: se invocan flotando y se ocultan a + /// voluntad; mientras están guardadas no viven en ningún escritorio. + scratchpad: Vec, } impl Default for Desktop { @@ -78,6 +81,7 @@ impl Desktop { windows: HashMap::new(), keymap, rules: Rules::default(), + scratchpad: Vec::new(), } } @@ -157,6 +161,7 @@ impl Desktop { } BodyEvent::WindowClosed { id } => { self.windows.remove(&id); + self.scratchpad.retain(|&w| w != id); for ws in &mut self.workspaces { ws.remove(id); } @@ -257,6 +262,47 @@ impl Desktop { } self.relayout() } + DesktopAction::SendToScratchpad => { + let Some(id) = self.workspaces[active].focused() else { + return Vec::new(); + }; + for ws in &mut self.workspaces { + ws.remove(id); + } + if !self.scratchpad.contains(&id) { + self.scratchpad.push(id); + } + self.relayout() + } + DesktopAction::ToggleScratchpad => { + // ¿Hay alguna ventana del scratchpad en el escritorio activo? + let shown: Vec = self.workspaces[active] + .windows() + .iter() + .copied() + .filter(|id| self.scratchpad.contains(id)) + .collect(); + if !shown.is_empty() { + for id in shown { + self.workspaces[active].remove(id); + } + self.relayout() + } else if let Some(&id) = self.scratchpad.first() { + // La traemos de donde esté y la mostramos flotando. + for ws in &mut self.workspaces { + ws.remove(id); + } + let rect = self + .screen() + .map(centered_float_rect) + .unwrap_or_else(|| Rect::new(100, 100, 800, 600)); + self.workspaces[active].add(id); + self.workspaces[active].set_floating(id, Some(rect)); + self.relayout() + } else { + Vec::new() + } + } DesktopAction::CycleLayout => { let next = self.workspaces[active].params().mode.next(); self.workspaces[active].set_mode(next); @@ -435,6 +481,20 @@ impl Desktop { }); } } + // Ventanas guardadas en el scratchpad — en ningún escritorio. + for &id in &self.scratchpad { + let stashed = !self.workspaces.iter().any(|ws| ws.windows().contains(&id)); + if stashed { + 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: 0, // 0 = guardada en el scratchpad + focused: false, + }); + } + } lines } } @@ -873,4 +933,64 @@ mod tests { assert_eq!(d.workspace_loads()[1], 1); assert_eq!(d.outputs().len(), 1); } + + // --- Scratchpad ---------------------------------------------------- + + #[test] + fn send_to_scratchpad_hides_the_focused_window() { + let mut d = desktop_with_screen(); + open(&mut d, 1); + open(&mut d, 2); // enfocada + d.apply(DesktopAction::SendToScratchpad); + assert_eq!(d.workspace_loads()[0], 1); // sólo queda la 1 + assert!(d.window_info(2).is_some()); // sigue registrada + } + + #[test] + fn toggle_scratchpad_shows_then_hides_the_stashed_window() { + let mut d = desktop_with_screen(); + open(&mut d, 1); + open(&mut d, 2); + d.apply(DesktopAction::SendToScratchpad); // guarda la 2 + assert_eq!(d.workspace_loads()[0], 1); + // Toggle la invoca, flotando. + let cmds = d.apply(DesktopAction::ToggleScratchpad); + assert!(places(&cmds).iter().find(|x| x.id == 2).unwrap().floating); + assert_eq!(d.workspace_loads()[0], 2); + // Toggle de nuevo la oculta. + d.apply(DesktopAction::ToggleScratchpad); + assert_eq!(d.workspace_loads()[0], 1); + } + + #[test] + fn a_scratchpad_window_follows_you_across_workspaces() { + let mut d = desktop_with_screen(); + open(&mut d, 1); + d.apply(DesktopAction::SendToScratchpad); + d.apply(DesktopAction::ToggleScratchpad); // mostrada en el escritorio 1 + assert_eq!(d.workspace_loads()[0], 1); + d.apply(DesktopAction::SwitchWorkspace(1)); // al escritorio 2 + d.apply(DesktopAction::ToggleScratchpad); // estaba en el 1 → la trae al 2 + assert_eq!(d.workspace_loads()[1], 1); + assert_eq!(d.workspace_loads()[0], 0); + } + + #[test] + fn closing_a_stashed_window_drops_it_from_the_scratchpad() { + let mut d = desktop_with_screen(); + open(&mut d, 1); + d.apply(DesktopAction::SendToScratchpad); + d.on_event(BodyEvent::WindowClosed { id: 1 }); + // Ya no hay nada que invocar. + assert!(d.apply(DesktopAction::ToggleScratchpad).is_empty()); + } + + #[test] + fn window_lines_show_a_stashed_window_as_workspace_zero() { + let mut d = desktop_with_screen(); + open(&mut d, 1); + d.apply(DesktopAction::SendToScratchpad); + let line = d.window_lines().into_iter().find(|l| l.id == 1).unwrap(); + assert_eq!(line.workspace, 0); + } } diff --git a/crates/modules/mirada/mirada-brain/src/keymap.rs b/crates/modules/mirada/mirada-brain/src/keymap.rs index 8318cb2..897cc0d 100644 --- a/crates/modules/mirada/mirada-brain/src/keymap.rs +++ b/crates/modules/mirada/mirada-brain/src/keymap.rs @@ -239,6 +239,8 @@ const KEYMAP_HEADER: &str = "\ // close-focused cierra la enfocada // toggle-float alterna flotante / teselada // toggle-fullscreen alterna pantalla completa +// send-to-scratchpad guarda la enfocada en el scratchpad +// toggle-scratchpad invoca / oculta la del scratchpad // cycle-layout siguiente modo de teselado // layout: master-stack | centered-master | spiral // grid | columns | rows | monocle diff --git a/vamos.txt b/vamos.txt index 8a240bb..41dff69 100644 --- a/vamos.txt +++ b/vamos.txt @@ -1082,5 +1082,13 @@ + Scratchpad — terminal desplegable (Super+`): + send-to-scratchpad guarda la ventana enfocada (sale del teselado, en ningún escritorio). + toggle-scratchpad la invoca flotando y centrada en el escritorio actual, o la oculta. + Una ventana del scratchpad te sigue: invocarla desde otro escritorio la trae consigo. + mirada-ctl windows la lista como «esc scratch». + + +