feat(mirada): scratchpad — ventana desplegable estilo terminal quake
Una ventana se puede guardar en el scratchpad (oculta, en ningún escritorio) e invocar a voluntad como overlay flotante — el patrón de la terminal desplegable. - Desktop.scratchpad: Vec<WindowId>. SendToScratchpad saca la ventana enfocada del teselado y la guarda; ToggleScratchpad (Super+`) la invoca flotando y centrada en el escritorio activo, o la oculta. - Invocarla desde otro escritorio la trae consigo (sale de donde estuviera). WindowClosed la quita del scratchpad. - window_lines marca las guardadas como workspace 0; mirada-ctl windows las lista como «esc scratch». Sin cambios de protocolo — una ventana del scratchpad invocada no es más que una flotante. Verificado end-to-end con headless-ctl. mirada-brain 58->63 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -84,10 +84,13 @@ fn print_windows(windows: &[WindowLine]) {
|
|||||||
}
|
}
|
||||||
for w in windows {
|
for w in windows {
|
||||||
let mark = if w.focused { '*' } else { ' ' };
|
let mark = if w.focused { '*' } else { ' ' };
|
||||||
println!(
|
// El escritorio 0 es el scratchpad (ventana guardada).
|
||||||
"{mark} id {:<4} esc {} {:<24} {}",
|
let ws = if w.workspace == 0 {
|
||||||
w.id, w.workspace, w.app_id, w.title
|
"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
|
close-focused cierra la ventana enfocada
|
||||||
toggle-float alterna flotante / teselada la enfocada
|
toggle-float alterna flotante / teselada la enfocada
|
||||||
toggle-fullscreen alterna pantalla completa en 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
|
cycle-layout pasa al siguiente modo de teselado
|
||||||
layout <modo> master-stack · centered-master · spiral
|
layout <modo> master-stack · centered-master · spiral
|
||||||
grid · columns · rows · monocle
|
grid · columns · rows · monocle
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
//! j / k foco siguiente/anterior , / . nmaster −/+
|
//! j / k foco siguiente/anterior , / . nmaster −/+
|
||||||
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
//! Shift+j / k mueve la enfocada 1..9 ir a escritorio
|
||||||
//! Enter promueve a maestra Ctrl+1..9 enviar 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
|
//! Los pips de escritorio y las ventanas del lienzo son **clicables**, y
|
||||||
@@ -301,6 +301,8 @@ impl Mirada {
|
|||||||
"h" => self.act(DesktopAction::ShrinkMaster),
|
"h" => self.act(DesktopAction::ShrinkMaster),
|
||||||
"l" => self.act(DesktopAction::GrowMaster),
|
"l" => self.act(DesktopAction::GrowMaster),
|
||||||
"o" => self.act(DesktopAction::FocusOutputNext),
|
"o" => self.act(DesktopAction::FocusOutputNext),
|
||||||
|
"`" if shift => self.act(DesktopAction::SendToScratchpad),
|
||||||
|
"`" => self.act(DesktopAction::ToggleScratchpad),
|
||||||
"enter" => self.act(DesktopAction::PromoteToMaster),
|
"enter" => self.act(DesktopAction::PromoteToMaster),
|
||||||
"," => self.act(DesktopAction::IncMaster),
|
"," => self.act(DesktopAction::IncMaster),
|
||||||
"." => self.act(DesktopAction::DecMaster),
|
"." => self.act(DesktopAction::DecMaster),
|
||||||
|
|||||||
@@ -128,6 +128,11 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
|||||||
el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
|
el escritorio pedido ya lo muestra otra salida); `FocusOutputNext`
|
||||||
(`Super+o`) mueve el foco entre monitores. El foco del teclado es
|
(`Super+o`) mueve el foco entre monitores. El foco del teclado es
|
||||||
único — sólo la ventana enfocada de la salida enfocada.
|
ú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<WindowId>`;
|
||||||
|
`mirada-ctl windows` la lista como `esc scratch`.
|
||||||
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
- **Layout y área maestra por el API** — los 7 modos se intercambian
|
||||||
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
(`SetLayout`/`CycleLayout`, `mirada-ctl layout spiral`); el área
|
||||||
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
maestra se redimensiona (`grow`/`shrink-master`, `Super+l`/`Super+h`);
|
||||||
@@ -170,7 +175,7 @@ a las ya abiertas.
|
|||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
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
|
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
||||||
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ pub enum DesktopAction {
|
|||||||
ToggleFloat,
|
ToggleFloat,
|
||||||
/// Alterna pantalla completa en la ventana enfocada.
|
/// Alterna pantalla completa en la ventana enfocada.
|
||||||
ToggleFullscreen,
|
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.
|
/// Pasa al siguiente modo de teselado.
|
||||||
CycleLayout,
|
CycleLayout,
|
||||||
/// Fija un modo de teselado concreto.
|
/// Fija un modo de teselado concreto.
|
||||||
@@ -106,6 +110,8 @@ impl fmt::Display for DesktopAction {
|
|||||||
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
DesktopAction::CloseFocused => f.write_str("close-focused"),
|
||||||
DesktopAction::ToggleFloat => f.write_str("toggle-float"),
|
DesktopAction::ToggleFloat => f.write_str("toggle-float"),
|
||||||
DesktopAction::ToggleFullscreen => f.write_str("toggle-fullscreen"),
|
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::CycleLayout => f.write_str("cycle-layout"),
|
||||||
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
|
DesktopAction::SetLayout(m) => write!(f, "layout:{}", layout_slug(*m)),
|
||||||
DesktopAction::GrowMaster => f.write_str("grow-master"),
|
DesktopAction::GrowMaster => f.write_str("grow-master"),
|
||||||
@@ -136,6 +142,8 @@ impl FromStr for DesktopAction {
|
|||||||
"close-focused" => Self::CloseFocused,
|
"close-focused" => Self::CloseFocused,
|
||||||
"toggle-float" => Self::ToggleFloat,
|
"toggle-float" => Self::ToggleFloat,
|
||||||
"toggle-fullscreen" => Self::ToggleFullscreen,
|
"toggle-fullscreen" => Self::ToggleFullscreen,
|
||||||
|
"send-to-scratchpad" => Self::SendToScratchpad,
|
||||||
|
"toggle-scratchpad" => Self::ToggleScratchpad,
|
||||||
"cycle-layout" => Self::CycleLayout,
|
"cycle-layout" => Self::CycleLayout,
|
||||||
"grow-master" => Self::GrowMaster,
|
"grow-master" => Self::GrowMaster,
|
||||||
"shrink-master" => Self::ShrinkMaster,
|
"shrink-master" => Self::ShrinkMaster,
|
||||||
@@ -197,6 +205,7 @@ pub fn default_keymap() -> Vec<(String, DesktopAction)> {
|
|||||||
("Super+q".into(), DesktopAction::CloseFocused),
|
("Super+q".into(), DesktopAction::CloseFocused),
|
||||||
("Super+f".into(), DesktopAction::ToggleFloat),
|
("Super+f".into(), DesktopAction::ToggleFloat),
|
||||||
("Super+Shift+f".into(), DesktopAction::ToggleFullscreen),
|
("Super+Shift+f".into(), DesktopAction::ToggleFullscreen),
|
||||||
|
("Super+`".into(), DesktopAction::ToggleScratchpad),
|
||||||
("Super+space".into(), DesktopAction::CycleLayout),
|
("Super+space".into(), DesktopAction::CycleLayout),
|
||||||
("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
|
("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
|
||||||
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
|
("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ pub struct WindowLine {
|
|||||||
pub id: WindowId,
|
pub id: WindowId,
|
||||||
pub app_id: String,
|
pub app_id: String,
|
||||||
pub title: 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,
|
pub workspace: usize,
|
||||||
/// `true` si es la ventana enfocada del escritorio activo.
|
/// `true` si es la ventana enfocada del escritorio activo.
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ pub struct Desktop {
|
|||||||
keymap: Keymap,
|
keymap: Keymap,
|
||||||
/// Reglas de ventana — escritorio/flotante por `app_id`/título.
|
/// Reglas de ventana — escritorio/flotante por `app_id`/título.
|
||||||
rules: Rules,
|
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<WindowId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Desktop {
|
impl Default for Desktop {
|
||||||
@@ -78,6 +81,7 @@ impl Desktop {
|
|||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
keymap,
|
keymap,
|
||||||
rules: Rules::default(),
|
rules: Rules::default(),
|
||||||
|
scratchpad: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +161,7 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
BodyEvent::WindowClosed { id } => {
|
BodyEvent::WindowClosed { id } => {
|
||||||
self.windows.remove(&id);
|
self.windows.remove(&id);
|
||||||
|
self.scratchpad.retain(|&w| w != id);
|
||||||
for ws in &mut self.workspaces {
|
for ws in &mut self.workspaces {
|
||||||
ws.remove(id);
|
ws.remove(id);
|
||||||
}
|
}
|
||||||
@@ -257,6 +262,47 @@ impl Desktop {
|
|||||||
}
|
}
|
||||||
self.relayout()
|
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<WindowId> = 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 => {
|
DesktopAction::CycleLayout => {
|
||||||
let next = self.workspaces[active].params().mode.next();
|
let next = self.workspaces[active].params().mode.next();
|
||||||
self.workspaces[active].set_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
|
lines
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,4 +933,64 @@ mod tests {
|
|||||||
assert_eq!(d.workspace_loads()[1], 1);
|
assert_eq!(d.workspace_loads()[1], 1);
|
||||||
assert_eq!(d.outputs().len(), 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,8 @@ const KEYMAP_HEADER: &str = "\
|
|||||||
// close-focused cierra la enfocada
|
// close-focused cierra la enfocada
|
||||||
// toggle-float alterna flotante / teselada
|
// toggle-float alterna flotante / teselada
|
||||||
// toggle-fullscreen alterna pantalla completa
|
// 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
|
// cycle-layout siguiente modo de teselado
|
||||||
// layout:<modo> master-stack | centered-master | spiral
|
// layout:<modo> master-stack | centered-master | spiral
|
||||||
// grid | columns | rows | monocle
|
// grid | columns | rows | monocle
|
||||||
|
|||||||
@@ -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».
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user