1
0
Fork 0
forked from wry/wry

feat: implement declarative scratchpads

This commit is contained in:
atagen 2026-06-03 16:51:26 +10:00
parent d756c8a6a2
commit b6502e1d8a
17 changed files with 549 additions and 78 deletions

View file

@ -648,6 +648,10 @@ impl ConfigClient {
self.send(&ClientMessage::SeatToggleScratchpad { seat, name });
}
pub fn seat_cycle_scratchpad(&self, seat: Seat, name: &str) {
self.send(&ClientMessage::SeatCycleScratchpad { seat, name });
}
pub fn window_send_to_scratchpad(&self, window: Window, name: &str) {
self.send(&ClientMessage::WindowSendToScratchpad { window, name });
}

View file

@ -294,6 +294,10 @@ pub enum ClientMessage<'a> {
seat: Seat,
name: &'a str,
},
SeatCycleScratchpad {
seat: Seat,
name: &'a str,
},
GetTimer {
name: &'a str,
},

View file

@ -477,11 +477,22 @@ impl Seat {
///
/// 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.
/// Scratchpad windows are always shown floating.
/// Use an empty string for the default scratchpad.
pub fn toggle_scratchpad(self, name: &str) {
get!().seat_toggle_scratchpad(self, name)
}
/// Cycles through the windows of a scratchpad, one at a time.
///
/// With nothing shown, the first window is brought up; each further invocation
/// hides the current window and shows the next; after the last window the
/// scratchpad is hidden again. Scratchpad windows are always shown floating.
/// Use an empty string for the default scratchpad.
pub fn cycle_scratchpad(self, name: &str) {
get!().seat_cycle_scratchpad(self, name)
}
/// Toggles whether the currently focused window is fullscreen.
pub fn toggle_fullscreen(self) {
let c = get!();

View file

@ -1118,6 +1118,14 @@ impl ConfigProxyHandler {
})
}
fn handle_seat_cycle_scratchpad(&self, seat: Seat, name: &str) -> Result<(), CphError> {
self.state.with_linear_layout_animations(|| {
let seat = self.get_seat(seat)?;
self.state.cycle_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)?;
@ -3021,6 +3029,9 @@ impl ConfigProxyHandler {
ClientMessage::SeatToggleScratchpad { seat, name } => self
.handle_seat_toggle_scratchpad(seat, name)
.wrn("seat_toggle_scratchpad")?,
ClientMessage::SeatCycleScratchpad { seat, name } => self
.handle_seat_cycle_scratchpad(seat, name)
.wrn("seat_cycle_scratchpad")?,
ClientMessage::GetConnector { ty, idx } => {
self.handle_get_connector(ty, idx).wrn("get_connector")?
}

View file

@ -298,6 +298,13 @@ impl TestConfig {
})
}
pub fn cycle_scratchpad(&self, seat: SeatId, name: &str) -> TestResult {
self.send(ClientMessage::SeatCycleScratchpad {
seat: Seat(seat.raw() as _),
name,
})
}
fn clear(&self) {
unsafe {
if let Some(srv) = self.srv.take() {

View file

@ -1,7 +1,7 @@
use {
crate::{
it::{test_error::TestResult, testrun::TestRun},
tree::Node,
tree::{Node, ToplevelNodeBase},
},
std::rc::Rc,
};
@ -45,6 +45,63 @@ async fn test(run: Rc<TestRun>) -> TestResult {
client.sync().await;
tassert!(win2.tl.server.node_visible());
tassert_eq!(ds.output.workspace.get().unwrap().name.as_str(), "3");
// Scratchpad windows are always shown floating.
tassert!(win2.tl.server.tl_data().parent_is_float.get());
// Park win2 again, then build a multi-window scratchpad and cycle it.
run.cfg.toggle_scratchpad(ds.seat.id(), "term")?;
client.sync().await;
tassert!(!win2.tl.server.node_visible());
// Build a three-window scratchpad. Each window is focused right after it is
// mapped, so sending the focused window parks them in a known order.
let cyc1 = client.create_window().await?;
cyc1.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
let cyc2 = client.create_window().await?;
cyc2.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
let cyc3 = client.create_window().await?;
cyc3.map2().await?;
run.cfg.send_to_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// Nothing shown: cycle brings up the first window (insertion order: cyc1).
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// Scratchpad windows are always shown floating.
tassert!(cyc1.tl.server.tl_data().parent_is_float.get());
// Cycle advances one at a time.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(cyc3.tl.server.node_visible());
// On the final window, the next cycle hides everything.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(!cyc1.tl.server.node_visible());
tassert!(!cyc2.tl.server.node_visible());
tassert!(!cyc3.tl.server.node_visible());
// And it wraps back to the first window.
run.cfg.cycle_scratchpad(ds.seat.id(), "cyc")?;
client.sync().await;
tassert!(cyc1.tl.server.node_visible());
Ok(())
}

View file

@ -114,7 +114,7 @@ use {
tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
PlaceholderNode, ScratchpadToplevelState, TearingMode, TileState, ToplevelData,
PlaceholderNode, TearingMode, TileState, ToplevelData,
ToplevelIdentifier, ToplevelNode, ToplevelNodeBase, Transform, VrrMode,
WorkspaceDisplayOrder, WorkspaceNode, WorkspaceNodeId, WsMoveConfig,
generic_node_visitor, move_ws_to_output, toplevel_hide_for_scratchpad,
@ -466,7 +466,6 @@ pub struct ScratchpadEntry {
node: Weak<dyn ToplevelNode>,
identifier: ToplevelIdentifier,
hidden: Cell<bool>,
restore: RefCell<Option<ScratchpadToplevelState>>,
}
impl ScratchpadEntry {
@ -1053,17 +1052,14 @@ impl State {
return;
}
let identifier = node.tl_data().identifier.get();
if !toplevel_hide_for_scratchpad(node.clone()) {
return;
}
let entry = Rc::new(ScratchpadEntry {
node: Rc::downgrade(&node),
identifier,
hidden: Cell::new(false),
restore: Default::default(),
hidden: Cell::new(true),
});
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() {
@ -1072,7 +1068,7 @@ impl State {
scratchpads
.entry(name.to_string())
.or_default()
.push(entry.clone());
.push(entry);
}
self.tree_changed();
}
@ -1084,29 +1080,19 @@ impl State {
return;
};
entries.retain(|entry| entry.alive());
// Prefer the currently-shown window; otherwise act on the most recent.
entries
.iter()
.rev()
.find(|entry| {
!entry.hidden.get() && entry.node().is_some_and(|node| node.node_visible())
})
.find(|entry| !entry.hidden.get())
.or_else(|| entries.last())
.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);
self.show_scratchpad_entry(seat, name, &entry);
} else if entry.node().is_some_and(|node| !node.node_visible()) {
self.move_scratchpad_entry_to_current_workspace(seat, &entry);
} else {
@ -1114,12 +1100,39 @@ impl State {
}
}
/// Cycles through the windows of a scratchpad, one at a time:
/// nothing shown -> first window -> ... -> last window -> nothing shown.
pub fn cycle_scratchpad(self: &Rc<Self>, seat: &Rc<WlSeatGlobal>, name: &str) {
let (current, next) = {
let mut scratchpads = self.scratchpads.borrow_mut();
let Some(entries) = scratchpads.get_mut(name) else {
return;
};
entries.retain(|entry| entry.alive());
match entries.iter().position(|entry| !entry.hidden.get()) {
// Nothing shown yet: bring up the first window.
None => (None, entries.first().cloned()),
// Hide the shown window and advance; on the last window, `next`
// is `None`, so the scratchpad toggles off.
Some(i) => (entries.get(i).cloned(), entries.get(i + 1).cloned()),
}
};
if let Some(current) = &current {
self.hide_scratchpad_entry(current);
}
if let Some(next) = &next {
self.show_scratchpad_entry(seat, name, next);
}
}
fn hide_scratchpad_entry(self: &Rc<Self>, entry: &Rc<ScratchpadEntry>) {
if entry.hidden.get() {
return;
}
let Some(node) = entry.node() else {
return;
};
if let Some(restore) = toplevel_hide_for_scratchpad(node) {
*entry.restore.borrow_mut() = Some(restore);
if toplevel_hide_for_scratchpad(node) {
entry.hidden.set(true);
self.tree_changed();
}
@ -1128,6 +1141,7 @@ impl State {
fn show_scratchpad_entry(
self: &Rc<Self>,
seat: &Rc<WlSeatGlobal>,
name: &str,
entry: &Rc<ScratchpadEntry>,
) {
if !entry.hidden.get() {
@ -1136,12 +1150,22 @@ impl State {
let Some(node) = entry.node() else {
return;
};
let restore = entry.restore.borrow();
let Some(restore) = restore.as_ref() else {
return;
// Only one window of a scratchpad is visible at a time.
let siblings: Vec<_> = {
let scratchpads = self.scratchpads.borrow();
scratchpads
.get(name)
.into_iter()
.flatten()
.filter(|sibling| !Rc::ptr_eq(sibling, entry) && !sibling.hidden.get())
.cloned()
.collect()
};
for sibling in siblings {
self.hide_scratchpad_entry(&sibling);
}
let ws = seat.get_fallback_output().ensure_workspace();
toplevel_restore_from_scratchpad(self, node.clone(), &ws, restore);
toplevel_restore_from_scratchpad(self, node.clone(), &ws);
entry.hidden.set(false);
node.node_do_focus(seat, Direction::Unspecified);
seat.maybe_schedule_warp_mouse_to_focus();

View file

@ -1324,29 +1324,25 @@ pub fn toplevel_set_workspace(state: &Rc<State>, tl: Rc<dyn ToplevelNode>, ws: &
}
}
pub struct ScratchpadToplevelState {
pub floating: bool,
pub fullscreen: bool,
pub workspace: Option<Rc<WorkspaceNode>>,
}
pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> Option<ScratchpadToplevelState> {
/// Removes a toplevel from the tree so it can be parked in a scratchpad.
///
/// Returns `true` if the window was hidden. A placeholder, a window without a
/// parent, or a window that refuses to leave fullscreen cannot be parked.
pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> bool {
if tl.node_is_placeholder() {
return None;
return false;
}
let data = tl.tl_data();
let scratchpad_state = ScratchpadToplevelState {
floating: data.parent_is_float.get(),
fullscreen: data.is_fullscreen.get(),
workspace: data.workspace.get(),
};
let workspace = data.workspace.get();
if data.is_fullscreen.get() {
tl.clone().tl_set_fullscreen(false, None);
if data.is_fullscreen.get() {
return None;
return false;
}
}
let parent = data.parent.get()?;
let Some(parent) = data.parent.get() else {
return false;
};
let kb_foci = collect_kb_foci(tl.clone());
parent.cnode_remove_child2(&*tl, true);
data.parent.take();
@ -1358,29 +1354,23 @@ pub fn toplevel_hide_for_scratchpad(tl: Rc<dyn ToplevelNode>) -> Option<Scratchp
data.property_changed(TL_CHANGED_WORKSPACE);
}
tl.tl_set_visible(false);
if let Some(workspace) = &scratchpad_state.workspace {
if let Some(workspace) = &workspace {
for seat in kb_foci {
workspace
.clone()
.node_do_focus(&seat, Direction::Unspecified);
}
}
Some(scratchpad_state)
true
}
/// Maps a parked scratchpad window back onto `ws`. Scratchpad windows always
/// return floating, regardless of how they were laid out before parking.
pub fn toplevel_restore_from_scratchpad(
state: &Rc<State>,
tl: Rc<dyn ToplevelNode>,
ws: &Rc<WorkspaceNode>,
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()));
}
let (width, height) = tl.tl_data().float_size(ws);
state.map_floating(tl.clone(), width, height, ws, None);
}

View file

@ -66,6 +66,7 @@ pub enum SimpleCommand {
SetFullscreen(bool),
SendToScratchpad,
ToggleScratchpad,
CycleScratchpad,
Forward(bool),
EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool),
@ -138,6 +139,9 @@ pub enum Action {
ToggleScratchpad {
name: String,
},
CycleScratchpad {
name: String,
},
Multi {
actions: Vec<Action>,
},

View file

@ -41,6 +41,7 @@ pub mod modified_keysym;
mod output;
mod output_match;
mod repeat_rate;
mod scratchpad;
pub mod shortcuts;
mod simple_im;
mod status;

View file

@ -119,6 +119,7 @@ impl ActionParser<'_> {
"exit-fullscreen" => SetFullscreen(false),
"send-to-scratchpad" => SendToScratchpad,
"toggle-scratchpad" => ToggleScratchpad,
"cycle-scratchpad" => CycleScratchpad,
"focus-parent" => FocusParent,
"close" => Close,
"disable-pointer-constraint" => DisablePointerConstraint,
@ -242,6 +243,15 @@ impl ActionParser<'_> {
Ok(Action::ToggleScratchpad { name })
}
fn parse_cycle_scratchpad(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let name = ext
.extract(opt(str("name")))?
.map(|name| name.value)
.unwrap_or("")
.to_string();
Ok(Action::CycleScratchpad { name })
}
fn parse_configure_connector(&mut self, ext: &mut Extractor<'_>) -> ParseResult<Self> {
let con = ext
.extract(val("connector"))?
@ -573,6 +583,7 @@ impl Parser for ActionParser<'_> {
"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),
"cycle-scratchpad" => self.parse_cycle_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),

View file

@ -28,6 +28,7 @@ use {
log_level::LogLevelParser,
output::OutputsParser,
repeat_rate::RepeatRateParser,
scratchpad::ScratchpadsParser,
shortcuts::{
ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError,
parse_modified_keysym_str,
@ -570,6 +571,13 @@ impl Parser for ConfigParser<'_> {
}
}
}
let mut scratchpads = vec![];
if let Some(value) = scratchpads_val {
match value.parse(&mut ScratchpadsParser(self.0)) {
Ok(v) => scratchpads = v,
Err(e) => log::warn!("Could not parse the scratchpads: {}", self.0.error(e)),
}
}
Ok(Config {
keymap,
repeat_rate,

View file

@ -0,0 +1,87 @@
use {
crate::{
config::{
Scratchpad,
context::Context,
extractor::{Extractor, ExtractorError, opt, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::exec::{ExecParser, ExecParserError},
},
toml::{
toml_span::{Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ScratchpadParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error(transparent)]
Exec(#[from] ExecParserError),
}
pub struct ScratchpadParser<'a>(pub &'a Context<'a>);
impl Parser for ScratchpadParser<'_> {
type Value = Scratchpad;
type Error = ScratchpadParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (name, exec_val) = ext.extract((str("name"), opt(val("exec"))))?;
let exec = match exec_val {
None => None,
Some(e) => Some(e.parse_map(&mut ExecParser(self.0))?),
};
Ok(Scratchpad {
name: name.value.to_string(),
exec,
})
}
}
pub struct ScratchpadsParser<'a>(pub &'a Context<'a>);
impl Parser for ScratchpadsParser<'_> {
type Value = Vec<Scratchpad>;
type Error = ScratchpadParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
let mut res = vec![];
for el in array {
match el.parse(&mut ScratchpadParser(self.0)) {
Ok(o) => res.push(o),
Err(e) => {
log::warn!("Could not parse scratchpad: {}", self.0.error(e));
}
}
}
Ok(res)
}
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
log::warn!(
"`scratchpads` value should be an array: {}",
self.0.error3(span)
);
ScratchpadParser(self.0)
.parse_table(span, table)
.map(|v| vec![v])
}
}

View file

@ -15,7 +15,7 @@ use {
config::{
Action, AnimationCurveConfig, ClientRule, Config, ConfigConnector, ConfigDrmDevice,
ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output,
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
OutputMatch, SimpleCommand, Status, Theme, WindowMatch, WindowRule, parse_config,
},
rules::{MatcherTemp, RuleMapper},
shortcuts::ModeState,
@ -175,6 +175,7 @@ impl Action {
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::CycleScratchpad => b.new(move || s.cycle_scratchpad("")),
SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
SimpleCommand::Close => window_or_seat!(s, s.close()),
SimpleCommand::DisablePointerConstraint => {
@ -310,6 +311,7 @@ impl Action {
}
Action::SendToScratchpad { name } => window_or_seat!(s, s.send_to_scratchpad(&name)),
Action::ToggleScratchpad { name } => b.new(move || s.toggle_scratchpad(&name)),
Action::CycleScratchpad { name } => b.new(move || s.cycle_scratchpad(&name)),
Action::ConfigureConnector { con } => b.new(move || {
for c in connectors() {
if con.match_.matches(c) {
@ -1461,6 +1463,43 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
window: Default::default(),
});
state.clear_modes_after_reload();
// Desugar `[[scratchpads]]` into spawn-on-graphics-init plus an internal
// window rule that parks the spawned window. Each spawned process gets a
// unique tag so only its own windows are captured, never other windows of
// the same application.
if !config.scratchpads.is_empty() {
let mut spawn_actions = vec![];
for (i, sp) in config.scratchpads.drain(..).enumerate() {
let Some(mut exec) = sp.exec else {
continue;
};
let tag = exec
.tag
.clone()
.unwrap_or_else(|| format!("__scratchpad.{i}.{}", sp.name));
exec.tag = Some(tag.clone());
spawn_actions.push(Action::Exec { exec });
config.window_rules.push(WindowRule {
name: None,
match_: WindowMatch {
tag: Some(tag),
..Default::default()
},
action: Some(Action::SendToScratchpad { name: sp.name }),
latch: None,
auto_focus: None,
initial_tile_state: None,
});
}
if !spawn_actions.is_empty() {
let mut actions = Vec::with_capacity(spawn_actions.len() + 1);
if let Some(existing) = config.on_graphics_initialized.take() {
actions.push(existing);
}
actions.extend(spawn_actions);
config.on_graphics_initialized = Some(Action::Multi { actions });
}
}
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
persistent.client_rules.set(client_rules);
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);

View file

@ -163,7 +163,7 @@
]
},
{
"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",
"description": "Sends the currently focused window to a scratchpad and hides it.\n\nA scratchpad can hold any number of windows. If `name` is omitted, the\ndefault 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": {
@ -179,7 +179,7 @@
]
},
{
"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",
"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.\nOnly one window of a scratchpad is shown at a time, and scratchpad windows are\nalways shown floating. If `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": {
@ -194,6 +194,22 @@
"type"
]
},
{
"description": "Cycles through the windows of a scratchpad, one at a time.\n\nWith no window shown, the first window is brought up. Each further invocation\nhides the current window and shows the next; after the last window the\nscratchpad is hidden again. Scratchpad windows are always shown floating.\nIf `name` is omitted, the default scratchpad is used.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-minus = { type = \"cycle-scratchpad\", name = \"terminal\" }\n ```\n",
"type": "object",
"properties": {
"type": {
"const": "cycle-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",
@ -1272,6 +1288,14 @@
"egui": {
"description": "Sets the egui settings of the compositor.\n",
"$ref": "#/$defs/Egui"
},
"scratchpads": {
"type": "array",
"description": "An array of pre-configured scratchpads.\n\nEach entry launches a program when the graphics are first initialized and\nimmediately parks its window in the named scratchpad. The window is captured\nvia a unique tag attached to the spawned process, so other windows of the\nsame application are never affected.\n\nUse a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows\nup; they are always shown floating.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n\n [[scratchpads]]\n name = \"notes\"\n exec = [\"obsidian\"]\n ```\n",
"items": {
"description": "",
"$ref": "#/$defs/Scratchpad"
}
}
},
"required": []
@ -2086,6 +2110,23 @@
},
"required": []
},
"Scratchpad": {
"description": "A pre-configured scratchpad whose program is launched at startup and parked\nin the scratchpad.\n\n- Example:\n\n ```toml\n [[scratchpads]]\n name = \"term\"\n exec = \"foot\"\n ```\n",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the scratchpad that the spawned window is parked in."
},
"exec": {
"description": "The program to launch when the graphics are first initialized.\n\nIf omitted, no program is launched and the scratchpad is only created on\ndemand by `send-to-scratchpad`.\n",
"$ref": "#/$defs/Exec"
}
},
"required": [
"name"
]
},
"SimpleActionName": {
"type": "string",
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
@ -2112,6 +2153,7 @@
"exit-fullscreen",
"send-to-scratchpad",
"toggle-scratchpad",
"cycle-scratchpad",
"focus-parent",
"close",
"disable-pointer-constraint",

View file

@ -289,11 +289,12 @@ This table is a tagged union. The variant is determined by the `type` field. It
- `send-to-scratchpad`:
Sends the currently focused window to a scratchpad and hides it.
If `name` is omitted, the default scratchpad is used.
A scratchpad can hold any number of windows. If `name` is omitted, the
default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-shift-minus = { type = "send-to-scratchpad", name = "terminal" }
@ -310,13 +311,14 @@ This table is a tagged union. The variant is determined by the `type` field. It
- `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.
Only one window of a scratchpad is shown at a time, and scratchpad windows are
always shown floating. If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "toggle-scratchpad", name = "terminal" }
@ -330,6 +332,30 @@ This table is a tagged union. The variant is determined by the `type` field. It
The value of this field should be a string.
- `cycle-scratchpad`:
Cycles through the windows of a scratchpad, one at a time.
With no window shown, the first window is brought up. Each further invocation
hides the current window and shows the next; after the last window the
scratchpad is hidden again. Scratchpad windows are always shown floating.
If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "cycle-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.
@ -1074,7 +1100,7 @@ The table has the following fields:
- `enabled` (optional):
Enables or disables window animations.
The default is `false`.
The value of this field should be a boolean.
@ -1082,7 +1108,7 @@ The table has the following fields:
- `duration-ms` (optional):
Sets the animation duration in milliseconds.
The default is `160`.
The value of this field should be a number.
@ -1092,7 +1118,7 @@ The table has the following fields:
- `style` (optional):
Sets the animation style used for tiled window movement animations.
The default is `multiphase`.
The value of this field should be a [AnimationStyle](#types-AnimationStyle).
@ -1100,7 +1126,7 @@ The table has the following fields:
- `curve` (optional):
Sets the animation curve.
The default is `ease-out`.
The value of this field should be a [AnimationCurve](#types-AnimationCurve).
@ -2336,11 +2362,11 @@ The table has the following fields:
- `animations` (optional):
Configures window animations.
Animations are disabled by default.
- Example:
```toml
[animations]
enabled = true
@ -2646,6 +2672,32 @@ The table has the following fields:
The value of this field should be a [Egui](#types-Egui).
- `scratchpads` (optional):
An array of pre-configured scratchpads.
Each entry launches a program when the graphics are first initialized and
immediately parks its window in the named scratchpad. The window is captured
via a unique tag attached to the spawned process, so other windows of the
same application are never affected.
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
up; they are always shown floating.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
[[scratchpads]]
name = "notes"
exec = ["obsidian"]
```
The value of this field should be an array of [Scratchpads](#types-Scratchpad).
<a name="types-Connector"></a>
### `Connector`
@ -4579,6 +4631,40 @@ The table has the following fields:
The value of this field should be a string.
<a name="types-Scratchpad"></a>
### `Scratchpad`
A pre-configured scratchpad whose program is launched at startup and parked
in the scratchpad.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
```
Values of this type should be tables.
The table has the following fields:
- `name` (required):
The name of the scratchpad that the spawned window is parked in.
The value of this field should be a string.
- `exec` (optional):
The program to launch when the graphics are first initialized.
If omitted, no program is launched and the scratchpad is only created on
demand by `send-to-scratchpad`.
The value of this field should be a [Exec](#types-Exec).
<a name="types-SimpleActionName"></a>
### `SimpleActionName`
@ -4702,6 +4788,10 @@ The string should have one of the following values:
Toggles the default scratchpad.
- `cycle-scratchpad`:
Cycles through the windows of the default scratchpad.
- `focus-parent`:
Focus the parent of the currently focused window.
@ -5883,3 +5973,5 @@ The table has the following fields:
The scaling mode of X windows.
The value of this field should be a [XScalingMode](#types-XScalingMode).

View file

@ -349,7 +349,8 @@ Action:
description: |
Sends the currently focused window to a scratchpad and hides it.
If `name` is omitted, the default scratchpad is used.
A scratchpad can hold any number of windows. If `name` is omitted, the
default scratchpad is used.
- Example:
@ -368,7 +369,8 @@ Action:
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.
Only one window of a scratchpad is shown at a time, and scratchpad windows are
always shown floating. If `name` is omitted, the default scratchpad is used.
- Example:
@ -381,6 +383,26 @@ Action:
description: The name of the scratchpad.
required: false
kind: string
cycle-scratchpad:
description: |
Cycles through the windows of a scratchpad, one at a time.
With no window shown, the first window is brought up. Each further invocation
hides the current window and shows the next; after the last window the
scratchpad is hidden again. Scratchpad windows are always shown floating.
If `name` is omitted, the default scratchpad is used.
- Example:
```toml
[shortcuts]
alt-minus = { type = "cycle-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.
@ -1116,6 +1138,8 @@ SimpleActionName:
description: Sends the currently focused window to the default scratchpad.
- value: toggle-scratchpad
description: Toggles the default scratchpad.
- value: cycle-scratchpad
description: Cycles through the windows of the default scratchpad.
- value: focus-parent
description: Focus the parent of the currently focused window.
- value: close
@ -3286,6 +3310,61 @@ Config:
required: false
description: |
Sets the egui settings of the compositor.
scratchpads:
kind: array
items:
ref: Scratchpad
required: false
description: |
An array of pre-configured scratchpads.
Each entry launches a program when the graphics are first initialized and
immediately parks its window in the named scratchpad. The window is captured
via a unique tag attached to the spawned process, so other windows of the
same application are never affected.
Use a `toggle-scratchpad` or `cycle-scratchpad` action to bring the windows
up; they are always shown floating.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
[[scratchpads]]
name = "notes"
exec = ["obsidian"]
```
Scratchpad:
kind: table
description: |
A pre-configured scratchpad whose program is launched at startup and parked
in the scratchpad.
- Example:
```toml
[[scratchpads]]
name = "term"
exec = "foot"
```
fields:
name:
kind: string
required: true
description: The name of the scratchpad that the spawned window is parked in.
exec:
ref: Exec
required: false
description: |
The program to launch when the graphics are first initialized.
If omitted, no program is launched and the scratchpad is only created on
demand by `send-to-scratchpad`.
Idle: