From b1ca98b48805e9a6e5aba34533168fcb133d7d00 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 15:33:02 +0200 Subject: [PATCH] config: add auto-focus window rule --- jay-config/src/_private/client.rs | 7 ++++ jay-config/src/_private/ipc.rs | 4 +++ jay-config/src/window.rs | 18 +++++++++++ src/config.rs | 9 ++++++ src/config/handler.rs | 32 +++++++++++++++++++ src/state.rs | 23 ++++++++----- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/window_rule.rs | 6 ++-- toml-config/src/rules.rs | 3 ++ toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 9 ++++++ toml-spec/spec/spec.yaml | 8 +++++ 12 files changed, 114 insertions(+), 10 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 63eda9cf..2991b43d 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1701,6 +1701,13 @@ impl ConfigClient { handler.cb = cb.clone(); } + pub fn set_window_matcher_auto_focus(&self, matcher: WindowMatcher, auto_focus: bool) { + self.send(&ClientMessage::SetWindowMatcherAutoFocus { + matcher, + auto_focus, + }); + } + pub fn set_window_matcher_latch_handler( &self, matcher: WindowMatcher, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 384c13ae..50d01c68 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -698,6 +698,10 @@ pub enum ClientMessage<'a> { EnableWindowMatcherEvents { matcher: WindowMatcher, }, + SetWindowMatcherAutoFocus { + matcher: WindowMatcher, + auto_focus: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index f7996d6f..222e1ac2 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -296,6 +296,16 @@ impl WindowCriterion<'_> { pub fn bind(self, cb: F) { self.to_matcher().bind(cb); } + + /// Sets whether newly mapped windows that match this criterion get the keyboard focus. + /// + /// If a window matches any criterion for which this is false, the window will not be + /// automatically focused. + /// + /// This leaks the matcher. + pub fn set_auto_focus(self, auto_focus: bool) { + self.to_matcher().set_auto_focus(auto_focus); + } } impl WindowMatcher { @@ -312,6 +322,14 @@ impl WindowMatcher { pub fn bind(self, cb: F) { get!().set_window_matcher_handler(self, cb); } + + /// Sets whether newly mapped windows that match this matcher get the keyboard focus. + /// + /// If a window matches any matcher for which this is false, the window will not be + /// automatically focused. + pub fn set_auto_focus(self, auto_focus: bool) { + get!().set_window_matcher_auto_focus(self, auto_focus); + } } impl MatchedWindow { diff --git a/src/config.rs b/src/config.rs index 841e3fad..7655568b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ use { config::handler::ConfigProxyHandler, ifs::wl_seat::SeatId, state::State, + tree::ToplevelData, utils::{ clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, @@ -161,6 +162,13 @@ impl ConfigProxy { handler.windows_to_tl_id.remove(&win); } } + + pub fn auto_focus(&self, data: &ToplevelData) -> bool { + let Some(handler) = self.handler.get() else { + return true; + }; + handler.auto_focus(data) + } } impl Drop for ConfigProxy { @@ -224,6 +232,7 @@ impl ConfigProxy { window_matcher_cache: Default::default(), window_matcher_leafs: Default::default(), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), + window_matcher_no_auto_focus: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 0b10eb2f..aa4996a1 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -124,6 +124,8 @@ pub(super) struct ConfigProxyHandler { pub window_matcher_cache: CriterionCache, pub window_matcher_leafs: CopyHashMap>, pub window_matcher_std_kinds: Rc, + pub window_matcher_no_auto_focus: + CopyHashMap>>, } pub struct Pollable { @@ -2027,6 +2029,7 @@ impl ConfigProxyHandler { fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) { self.window_matchers.remove(&matcher); self.window_matcher_leafs.remove(&matcher); + self.window_matcher_no_auto_focus.remove(&matcher); } fn handle_enable_window_matcher_events( @@ -2056,6 +2059,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_matcher_auto_focus( + &self, + matcher: WindowMatcher, + auto_focus: bool, + ) -> Result<(), CphError> { + if auto_focus { + self.window_matcher_no_auto_focus.remove(&matcher); + } else { + let m = self.get_window_matcher(matcher)?; + self.window_matcher_no_auto_focus.set(matcher, m); + } + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2861,9 +2878,24 @@ impl ConfigProxyHandler { ClientMessage::EnableWindowMatcherEvents { matcher } => self .handle_enable_window_matcher_events(matcher) .wrn("enable_window_matcher_events")?, + ClientMessage::SetWindowMatcherAutoFocus { + matcher, + auto_focus, + } => self + .handle_set_window_matcher_auto_focus(matcher, auto_focus) + .wrn("set_window_matcher_auto_focus")?, } Ok(()) } + + pub fn auto_focus(&self, data: &ToplevelData) -> bool { + for matcher in self.window_matcher_no_auto_focus.lock().values() { + if matcher.node.pull(data) { + return false; + } + } + true + } } #[derive(Debug, Error)] diff --git a/src/state.rs b/src/state.rs index f870ac9e..e93e67fe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -665,11 +665,7 @@ impl State { pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); self.do_map_tiled(seat.as_deref(), node.clone()); - if node.node_visible() { - if let Some(seat) = seat { - node.node_do_focus(&seat, Direction::Unspecified); - } - } + self.focus_after_map(node, seat.as_deref()); } fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { @@ -739,11 +735,22 @@ impl State { Rect::new_sized(x1, y1, width, height).unwrap() }; FloatNode::new(self, workspace, position, node.clone()); - if node.node_visible() { - if let Some(seat) = self.seat_queue.last() { - node.node_do_focus(&seat, Direction::Unspecified); + self.focus_after_map(node, self.seat_queue.last().as_deref()); + } + + fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { + if !node.node_visible() { + return; + } + let Some(seat) = seat else { + return; + }; + if let Some(config) = self.config.get() { + if !config.auto_focus(node.tl_data()) { + return; } } + node.node_do_focus(&seat, Direction::Unspecified); } pub fn show_workspace(&self, seat: &Rc, name: &str) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 553ec51b..52f18fde 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -248,6 +248,7 @@ pub struct WindowRule { pub match_: WindowMatch, pub action: Option, pub latch: Option, + pub auto_focus: Option, } #[derive(Default, Debug, Clone)] diff --git a/toml-config/src/config/parsers/window_rule.rs b/toml-config/src/config/parsers/window_rule.rs index a31ab978..21405e60 100644 --- a/toml-config/src/config/parsers/window_rule.rs +++ b/toml-config/src/config/parsers/window_rule.rs @@ -3,7 +3,7 @@ use { config::{ WindowMatch, WindowRule, context::Context, - extractor::{Extractor, ExtractorError, opt, str, val}, + extractor::{Extractor, ExtractorError, bol, opt, recover, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::{ActionParser, ActionParserError}, @@ -47,11 +47,12 @@ impl Parser for WindowRuleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (name, match_val, action_val, latch_val) = ext.extract(( + let (name, match_val, action_val, latch_val, auto_focus) = ext.extract(( opt(str("name")), opt(val("match")), opt(val("action")), opt(val("latch")), + recover(opt(bol("auto-focus"))), ))?; let mut action = None; if let Some(value) = action_val { @@ -78,6 +79,7 @@ impl Parser for WindowRuleParser<'_> { match_, action, latch, + auto_focus: auto_focus.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index f19d31fc..93ee47e2 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -333,6 +333,9 @@ impl Rule for WindowRule { }); } } + if let Some(auto_focus) = self.auto_focus { + matcher.set_auto_focus(auto_focus); + } } fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index d248c695..7a91ce7b 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1904,6 +1904,10 @@ "latch": { "description": "An action to execute when a window no longer matches the criteria.", "$ref": "#/$defs/Action" + }, + "auto-focus": { + "type": "boolean", + "description": "Whether newly mapped windows that match this rule get the keyboard focus.\n\nIf a window matches any rule for which this is false, the window will not be\nautomatically focused.\n" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 2e249c4d..c86bc249 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4203,6 +4203,15 @@ The table has the following fields: The value of this field should be a [Action](#types-Action). +- `auto-focus` (optional): + + Whether newly mapped windows that match this rule get the keyboard focus. + + If a window matches any rule for which this is false, the window will not be + automatically focused. + + The value of this field should be a boolean. + ### `WindowTypeMask` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 201a3c17..73914d9a 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3368,6 +3368,14 @@ WindowRule: ref: Action required: false description: An action to execute when a window no longer matches the criteria. + auto-focus: + kind: boolean + required: false + description: | + Whether newly mapped windows that match this rule get the keyboard focus. + + If a window matches any rule for which this is false, the window will not be + automatically focused. WindowMatch: