From 290b290fdf6ba834eebefbea8370b911bbb5bc5d Mon Sep 17 00:00:00 2001 From: atagen Date: Sun, 31 May 2026 17:23:56 +1000 Subject: [PATCH] Implement scratchpad window toggling --- jay-config/src/_private/client.rs | 12 ++ jay-config/src/_private/ipc.rs | 12 ++ jay-config/src/input.rs | 16 +++ jay-config/src/window.rs | 7 ++ src/compositor.rs | 1 + src/config/handler.rs | 35 ++++++ src/it/test_config.rs | 14 +++ src/it/tests.rs | 2 + src/it/tests/t0055_scratchpad.rs | 50 ++++++++ src/state.rs | 147 ++++++++++++++++++++++- src/tree/toplevel.rs | 61 ++++++++++ toml-config/src/config.rs | 8 ++ toml-config/src/config/parsers/action.rs | 22 ++++ toml-config/src/lib.rs | 4 + toml-spec/spec/spec.generated.json | 34 ++++++ toml-spec/spec/spec.generated.md | 53 ++++++++ toml-spec/spec/spec.yaml | 40 ++++++ 17 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 src/it/tests/t0055_scratchpad.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 7c78abac..57075e68 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -640,6 +640,18 @@ impl ConfigClient { self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); } + pub fn seat_send_to_scratchpad(&self, seat: Seat, name: &str) { + self.send(&ClientMessage::SeatSendToScratchpad { seat, name }); + } + + pub fn seat_toggle_scratchpad(&self, seat: Seat, name: &str) { + self.send(&ClientMessage::SeatToggleScratchpad { seat, name }); + } + + pub fn window_send_to_scratchpad(&self, window: Window, name: &str) { + self.send(&ClientMessage::WindowSendToScratchpad { window, name }); + } + pub fn seat_split(&self, seat: Seat) -> Axis { let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index c61c1af6..20ca2269 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -286,6 +286,14 @@ pub enum ClientMessage<'a> { seat: Seat, workspace: Workspace, }, + SeatSendToScratchpad { + seat: Seat, + name: &'a str, + }, + SeatToggleScratchpad { + seat: Seat, + name: &'a str, + }, GetTimer { name: &'a str, }, @@ -687,6 +695,10 @@ pub enum ClientMessage<'a> { window: Window, workspace: Workspace, }, + WindowSendToScratchpad { + window: Window, + name: &'a str, + }, SetWindowFullscreen { window: Window, fullscreen: bool, diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index dbdef1ba..560197c4 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -466,6 +466,22 @@ impl Seat { get!().set_seat_workspace(self, workspace) } + /// Sends the currently focused window to a scratchpad. + /// + /// Use an empty string for the default scratchpad. + pub fn send_to_scratchpad(self, name: &str) { + get!().seat_send_to_scratchpad(self, name) + } + + /// Toggles a scratchpad. + /// + /// If the scratchpad has a visible window, that window is hidden. Otherwise, the + /// most recently hidden window in the scratchpad is shown on the current workspace. + /// Use an empty string for the default scratchpad. + pub fn toggle_scratchpad(self, name: &str) { + get!().seat_toggle_scratchpad(self, name) + } + /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { let c = get!(); diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 662cda44..96e4d3b1 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -205,6 +205,13 @@ impl Window { get!().set_window_workspace(self, workspace) } + /// Sends the window to a scratchpad. + /// + /// Use an empty string for the default scratchpad. + pub fn send_to_scratchpad(self, name: &str) { + get!().window_send_to_scratchpad(self, name) + } + /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { self.set_fullscreen(!self.fullscreen()) diff --git a/src/compositor.rs b/src/compositor.rs index 11f23808..4dd47342 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -403,6 +403,7 @@ fn start_compositor2( bo_drop_queue: Rc::new(ObjectDropQueue::new(&ring)), virtual_outputs: Default::default(), clean_logs_older_than: Default::default(), + scratchpads: Default::default(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index 9a11acab..f6bc224f 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1100,6 +1100,24 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_seat_send_to_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { + self.state.with_linear_layout_animations(|| { + let seat = self.get_seat(seat)?; + if let Some(toplevel) = seat.get_keyboard_node().node_toplevel() { + self.state.send_to_scratchpad(name, toplevel); + } + Ok(()) + }) + } + + fn handle_seat_toggle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> { + self.state.with_linear_layout_animations(|| { + let seat = self.get_seat(seat)?; + self.state.toggle_scratchpad(&seat, name); + Ok(()) + }) + } + fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { let window = self.get_window(window)?; let name = self.get_workspace(ws)?; @@ -1114,6 +1132,14 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_window_send_to_scratchpad(&self, window: Window, name: &str) -> Result<(), CphError> { + self.state.with_linear_layout_animations(|| { + let window = self.get_window(window)?; + self.state.send_to_scratchpad(name, window); + Ok(()) + }) + } + fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; let name = dev.device.name(); @@ -2989,6 +3015,12 @@ impl ConfigProxyHandler { ClientMessage::SetSeatWorkspace { seat, workspace } => self .handle_set_seat_workspace(seat, workspace) .wrn("set_seat_workspace")?, + ClientMessage::SeatSendToScratchpad { seat, name } => self + .handle_seat_send_to_scratchpad(seat, name) + .wrn("seat_send_to_scratchpad")?, + ClientMessage::SeatToggleScratchpad { seat, name } => self + .handle_seat_toggle_scratchpad(seat, name) + .wrn("seat_toggle_scratchpad")?, ClientMessage::GetConnector { ty, idx } => { self.handle_get_connector(ty, idx).wrn("get_connector")? } @@ -3373,6 +3405,9 @@ impl ConfigProxyHandler { ClientMessage::SetWindowWorkspace { window, workspace } => self .handle_set_window_workspace(window, workspace) .wrn("set_window_workspace")?, + ClientMessage::WindowSendToScratchpad { window, name } => self + .handle_window_send_to_scratchpad(window, name) + .wrn("window_send_to_scratchpad")?, ClientMessage::SetWindowFullscreen { window, fullscreen } => self .handle_set_window_fullscreen(window, fullscreen) .wrn("set_window_fullscreen")?, diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 7691bbcd..5eba8aca 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -284,6 +284,20 @@ impl TestConfig { }) } + pub fn send_to_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { + self.send(ClientMessage::SeatSendToScratchpad { + seat: Seat(seat.raw() as _), + name, + }) + } + + pub fn toggle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult { + self.send(ClientMessage::SeatToggleScratchpad { + seat: Seat(seat.raw() as _), + name, + }) + } + fn clear(&self) { unsafe { if let Some(srv) = self.srv.take() { diff --git a/src/it/tests.rs b/src/it/tests.rs index 3e1e502c..35b6be97 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -86,6 +86,7 @@ mod t0052_bar; mod t0053_theme; mod t0054_subsurface_already_attached; mod t0055_autotiling; +mod t0055_scratchpad; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -160,5 +161,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0053_theme, t0054_subsurface_already_attached, t0055_autotiling, + t0055_scratchpad, } } diff --git a/src/it/tests/t0055_scratchpad.rs b/src/it/tests/t0055_scratchpad.rs new file mode 100644 index 00000000..2519335a --- /dev/null +++ b/src/it/tests/t0055_scratchpad.rs @@ -0,0 +1,50 @@ +use { + crate::{ + it::{test_error::TestResult, testrun::TestRun}, + tree::Node, + }, + std::rc::Rc, +}; + +testcase!(); + +async fn test(run: Rc) -> TestResult { + let ds = run.create_default_setup().await?; + + let client = run.create_client().await?; + let win1 = client.create_window().await?; + win1.map2().await?; + let win2 = client.create_window().await?; + win2.map2().await?; + + run.cfg.send_to_scratchpad(ds.seat.id(), "term")?; + client.sync().await; + tassert!(win1.tl.server.node_visible()); + tassert!(!win2.tl.server.node_visible()); + + run.cfg.show_workspace(ds.seat.id(), "2")?; + run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; + client.sync().await; + tassert!(win2.tl.server.node_visible()); + tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); + + run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; + client.sync().await; + tassert!(!win2.tl.server.node_visible()); + + run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; + client.sync().await; + tassert!(win2.tl.server.node_visible()); + tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "2"); + + run.cfg.show_workspace(ds.seat.id(), "3")?; + client.sync().await; + tassert!(!win2.tl.server.node_visible()); + + run.cfg.toggle_scratchpad(ds.seat.id(), "term")?; + client.sync().await; + tassert!(win2.tl.server.node_visible()); + tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3"); + + Ok(()) +} diff --git a/src/state.rs b/src/state.rs index a7dad1d5..d10ff054 100644 --- a/src/state.rs +++ b/src/state.rs @@ -114,9 +114,11 @@ use { tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode, FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode, - PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier, - ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder, - WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output, + PlaceholderNode, ScratchpadToplevelState, TearingMode, TileState, ToplevelData, + ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode, + WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig, + generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad, + toplevel_restore_from_scratchpad, toplevel_set_workspace, }, udmabuf::UdmabufHolder, utils::{ @@ -412,6 +414,7 @@ pub struct State { pub bo_drop_queue: Rc>>, pub virtual_outputs: VirtualOutputs, pub clean_logs_older_than: Cell>, + pub scratchpads: RefCell>>>, } // impl Drop for State { @@ -459,6 +462,28 @@ pub struct IdleState { pub in_grace_period: Cell, } +pub struct ScratchpadEntry { + node: Weak, + identifier: ToplevelIdentifier, + hidden: Cell, + restore: RefCell>, +} + +impl ScratchpadEntry { + fn alive(&self) -> bool { + self.node().is_some() + } + + fn node(&self) -> Option> { + let node = self.node.upgrade()?; + if node.tl_data().identifier.get() == self.identifier { + Some(node) + } else { + None + } + } +} + impl IdleState { pub fn set_timeout(&self, state: &State, timeout: Duration) { self.timeout.set(timeout); @@ -1023,6 +1048,121 @@ impl State { float } + pub fn send_to_scratchpad(self: &Rc, name: &str, node: Rc) { + if node.node_is_placeholder() { + return; + } + let identifier = node.tl_data().identifier.get(); + let entry = Rc::new(ScratchpadEntry { + node: Rc::downgrade(&node), + identifier, + hidden: Cell::new(false), + restore: Default::default(), + }); + let Some(restore) = toplevel_hide_for_scratchpad(node) else { + return; + }; + *entry.restore.borrow_mut() = Some(restore); + entry.hidden.set(true); + { + let mut scratchpads = self.scratchpads.borrow_mut(); + for entries in scratchpads.values_mut() { + entries.retain(|entry| entry.alive() && entry.identifier != identifier); + } + scratchpads + .entry(name.to_string()) + .or_default() + .push(entry.clone()); + } + self.tree_changed(); + } + + pub fn toggle_scratchpad(self: &Rc, seat: &Rc, name: &str) { + let entry = { + let mut scratchpads = self.scratchpads.borrow_mut(); + let Some(entries) = scratchpads.get_mut(name) else { + return; + }; + entries.retain(|entry| entry.alive()); + entries + .iter() + .rev() + .find(|entry| { + !entry.hidden.get() && entry.node().is_some_and(|node| node.node_visible()) + }) + .cloned() + .or_else(|| { + entries + .iter() + .rev() + .find(|entry| { + entry.hidden.get() + || entry.node().is_some_and(|node| !node.node_visible()) + }) + .cloned() + }) + }; + let Some(entry) = entry else { + return; + }; + if entry.hidden.get() { + self.show_scratchpad_entry(seat, &entry); + } else if entry.node().is_some_and(|node| !node.node_visible()) { + self.move_scratchpad_entry_to_current_workspace(seat, &entry); + } else { + self.hide_scratchpad_entry(&entry); + } + } + + fn hide_scratchpad_entry(self: &Rc, entry: &Rc) { + let Some(node) = entry.node() else { + return; + }; + if let Some(restore) = toplevel_hide_for_scratchpad(node) { + *entry.restore.borrow_mut() = Some(restore); + entry.hidden.set(true); + self.tree_changed(); + } + } + + fn show_scratchpad_entry( + self: &Rc, + seat: &Rc, + entry: &Rc, + ) { + if !entry.hidden.get() { + return; + } + let Some(node) = entry.node() else { + return; + }; + let restore = entry.restore.borrow(); + let Some(restore) = restore.as_ref() else { + return; + }; + let ws = seat.get_fallback_output().ensure_workspace(); + toplevel_restore_from_scratchpad(self, node.clone(), &ws, restore); + entry.hidden.set(false); + node.node_do_focus(seat, Direction::Unspecified); + seat.maybe_schedule_warp_mouse_to_focus(); + self.tree_changed(); + } + + fn move_scratchpad_entry_to_current_workspace( + self: &Rc, + seat: &Rc, + entry: &Rc, + ) { + let Some(node) = entry.node() else { + return; + }; + let ws = seat.get_fallback_output().ensure_workspace(); + toplevel_set_workspace(self, node.clone(), &ws); + node.node_do_focus(seat, Direction::Unspecified); + seat.maybe_schedule_warp_mouse_to_focus(); + self.tree_changed(); + } + fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { if !node.node_visible() { return; @@ -1298,6 +1438,7 @@ impl State { self.node_at_tree.borrow_mut().clear(); self.position_hint_requests.clear(); self.pending_warp_mouse_to_focus.clear(); + self.scratchpads.borrow_mut().clear(); self.head_managers.clear(); self.head_managers_async.clear(); self.const_40hz_latch.clear(); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 7fff564b..85661202 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1323,3 +1323,64 @@ pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: & tl.tl_set_fullscreen(true, Some(ws.clone())); } } + +pub struct ScratchpadToplevelState { + pub floating: bool, + pub fullscreen: bool, + pub workspace: Option>, +} + +pub fn toplevel_hide_for_scratchpad(tl: Rc) -> Option { + if tl.node_is_placeholder() { + return None; + } + let data = tl.tl_data(); + let scratchpad_state = ScratchpadToplevelState { + floating: data.parent_is_float.get(), + fullscreen: data.is_fullscreen.get(), + workspace: data.workspace.get(), + }; + if data.is_fullscreen.get() { + tl.clone().tl_set_fullscreen(false, None); + if data.is_fullscreen.get() { + return None; + } + } + let parent = data.parent.get()?; + let kb_foci = collect_kb_foci(tl.clone()); + parent.cnode_remove_child2(&*tl, true); + data.parent.take(); + data.float.take(); + if data.parent_is_float.replace(false) { + data.property_changed(TL_CHANGED_FLOATING); + } + if data.workspace.take().is_some() { + data.property_changed(TL_CHANGED_WORKSPACE); + } + tl.tl_set_visible(false); + if let Some(workspace) = &scratchpad_state.workspace { + for seat in kb_foci { + workspace + .clone() + .node_do_focus(&seat, Direction::Unspecified); + } + } + Some(scratchpad_state) +} + +pub fn toplevel_restore_from_scratchpad( + state: &Rc, + tl: Rc, + ws: &Rc, + scratchpad_state: &ScratchpadToplevelState, +) { + if scratchpad_state.floating { + let (width, height) = tl.tl_data().float_size(ws); + state.map_floating(tl.clone(), width, height, ws, None); + } else { + state.map_tiled_on(tl.clone(), ws); + } + if scratchpad_state.fullscreen && ws.fullscreen.is_none() { + tl.tl_set_fullscreen(true, Some(ws.clone())); + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 0eed4a21..894cb072 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -64,6 +64,8 @@ pub enum SimpleCommand { SetFloating(bool), ToggleFullscreen, SetFullscreen(bool), + SendToScratchpad, + ToggleScratchpad, Forward(bool), EnableWindowManagement(bool), SetFloatAboveFullscreen(bool), @@ -130,6 +132,12 @@ pub enum Action { MoveToWorkspace { name: String, }, + SendToScratchpad { + name: String, + }, + ToggleScratchpad { + name: String, + }, Multi { actions: Vec, }, diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 7581198d..1afd8740 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -117,6 +117,8 @@ impl ActionParser<'_> { "toggle-fullscreen" => ToggleFullscreen, "enter-fullscreen" => SetFullscreen(true), "exit-fullscreen" => SetFullscreen(false), + "send-to-scratchpad" => SendToScratchpad, + "toggle-scratchpad" => ToggleScratchpad, "focus-parent" => FocusParent, "close" => Close, "disable-pointer-constraint" => DisablePointerConstraint, @@ -222,6 +224,24 @@ impl ActionParser<'_> { Ok(Action::MoveToWorkspace { name }) } + fn parse_send_to_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let name = ext + .extract(opt(str("name")))? + .map(|name| name.value) + .unwrap_or("") + .to_string(); + Ok(Action::SendToScratchpad { name }) + } + + fn parse_toggle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult { + let name = ext + .extract(opt(str("name")))? + .map(|name| name.value) + .unwrap_or("") + .to_string(); + Ok(Action::ToggleScratchpad { name }) + } + fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult { let con = ext .extract(val("connector"))? @@ -551,6 +571,8 @@ impl Parser for ActionParser<'_> { "switch-to-vt" => self.parse_switch_to_vt(&mut ext), "show-workspace" => self.parse_show_workspace(&mut ext), "move-to-workspace" => self.parse_move_to_workspace(&mut ext), + "send-to-scratchpad" => self.parse_send_to_scratchpad(&mut ext), + "toggle-scratchpad" => self.parse_toggle_scratchpad(&mut ext), "configure-connector" => self.parse_configure_connector(&mut ext), "configure-input" => self.parse_configure_input(&mut ext), "configure-output" => self.parse_configure_output(&mut ext), diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index d8bfea89..cc09047b 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -173,6 +173,8 @@ impl Action { SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), + SimpleCommand::SendToScratchpad => window_or_seat!(s, s.send_to_scratchpad("")), + SimpleCommand::ToggleScratchpad => b.new(move || s.toggle_scratchpad("")), SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::Close => window_or_seat!(s, s.close()), SimpleCommand::DisablePointerConstraint => { @@ -306,6 +308,8 @@ impl Action { let workspace = get_workspace(&name); window_or_seat!(s, s.set_workspace(workspace)) } + Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)), + Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)), Action::ConfigureConnector { con } => b.new(move || { for c in connectors() { if con.match_.matches(c) { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4d6cb2bf..efed4522 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -162,6 +162,38 @@ "name" ] }, + { + "description": "Sends the currently focused window to a scratchpad and hides it.\n\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-shift-minus = { type = \"send-to-scratchpad\", name = \"terminal\" }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "send-to-scratchpad" + }, + "name": { + "type": "string", + "description": "The name of the scratchpad." + } + }, + "required": [ + "type" + ] + }, + { + "description": "Toggles a scratchpad.\n\nIf the scratchpad has a visible window, that window is hidden. Otherwise, the\nmost recently hidden window in the scratchpad is shown on the current workspace.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"toggle-scratchpad\", name = \"terminal\" }\n ```\n", + "type": "object", + "properties": { + "type": { + "const": "toggle-scratchpad" + }, + "name": { + "type": "string", + "description": "The name of the scratchpad." + } + }, + "required": [ + "type" + ] + }, { "description": "Moves a workspace to a different output.\n\n- Example 1: Move a specific workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", workspace = \"1\", output.name = \"right\" }\n ```\n\n- Example 2: Move the current workspace to a named output\n\n ```toml\n [shortcuts]\n alt-F1 = { type = \"move-to-output\", output.name = \"right\" }\n ```\n\n- Example 3: Move the current workspace to the output on the right (directional)\n\n ```toml\n [shortcuts]\n \"logo+ctrl+shift+Right\" = { type = \"move-to-output\", direction = \"right\" }\n \"logo+ctrl+shift+Left\" = { type = \"move-to-output\", direction = \"left\" }\n \"logo+ctrl+shift+Up\" = { type = \"move-to-output\", direction = \"up\" }\n \"logo+ctrl+shift+Down\" = { type = \"move-to-output\", direction = \"down\" }\n ```\n", "type": "object", @@ -2078,6 +2110,8 @@ "toggle-fullscreen", "enter-fullscreen", "exit-fullscreen", + "send-to-scratchpad", + "toggle-scratchpad", "focus-parent", "close", "disable-pointer-constraint", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 1a9d82a8..df88e7c4 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -286,6 +286,50 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. +- `send-to-scratchpad`: + + Sends the currently focused window to a scratchpad and hides it. + + If `name` is omitted, the default scratchpad is used. + + - Example: + + ```toml + [shortcuts] + alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } + ``` + + The table has the following fields: + + - `name` (optional): + + The name of the scratchpad. + + The value of this field should be a string. + +- `toggle-scratchpad`: + + Toggles a scratchpad. + + If the scratchpad has a visible window, that window is hidden. Otherwise, the + most recently hidden window in the scratchpad is shown on the current workspace. + If `name` is omitted, the default scratchpad is used. + + - Example: + + ```toml + [shortcuts] + alt-minus = { type = "toggle-scratchpad", name = "terminal" } + ``` + + The table has the following fields: + + - `name` (optional): + + The name of the scratchpad. + + The value of this field should be a string. + - `move-to-output`: Moves a workspace to a different output. @@ -1007,6 +1051,7 @@ The string should have one of the following values: supported plan exists. + ### `Animations` @@ -4649,6 +4694,14 @@ The string should have one of the following values: Makes the currently focused window windowed. +- `send-to-scratchpad`: + + Sends the currently focused window to the default scratchpad. + +- `toggle-scratchpad`: + + Toggles the default scratchpad. + - `focus-parent`: Focus the parent of the currently focused window. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 7bc2b970..49731ad8 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -345,6 +345,42 @@ Action: description: The name of the workspace. required: true kind: string + send-to-scratchpad: + description: | + Sends the currently focused window to a scratchpad and hides it. + + If `name` is omitted, the default scratchpad is used. + + - Example: + + ```toml + [shortcuts] + alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" } + ``` + fields: + name: + description: The name of the scratchpad. + required: false + kind: string + toggle-scratchpad: + description: | + Toggles a scratchpad. + + If the scratchpad has a visible window, that window is hidden. Otherwise, the + most recently hidden window in the scratchpad is shown on the current workspace. + If `name` is omitted, the default scratchpad is used. + + - Example: + + ```toml + [shortcuts] + alt-minus = { type = "toggle-scratchpad", name = "terminal" } + ``` + fields: + name: + description: The name of the scratchpad. + required: false + kind: string move-to-output: description: | Moves a workspace to a different output. @@ -1076,6 +1112,10 @@ SimpleActionName: description: Makes the currently focused window fullscreen. - value: exit-fullscreen description: Makes the currently focused window windowed. + - value: send-to-scratchpad + description: Sends the currently focused window to the default scratchpad. + - value: toggle-scratchpad + description: Toggles the default scratchpad. - value: focus-parent description: Focus the parent of the currently focused window. - value: close