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

@ -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);