1
0
Fork 0
forked from wry/wry

config: add auto-focus window rule

This commit is contained in:
Julian Orth 2025-05-03 15:33:02 +02:00
parent 51e752992f
commit b1ca98b488
12 changed files with 114 additions and 10 deletions

View file

@ -1701,6 +1701,13 @@ impl ConfigClient {
handler.cb = cb.clone(); 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( pub fn set_window_matcher_latch_handler(
&self, &self,
matcher: WindowMatcher, matcher: WindowMatcher,

View file

@ -698,6 +698,10 @@ pub enum ClientMessage<'a> {
EnableWindowMatcherEvents { EnableWindowMatcherEvents {
matcher: WindowMatcher, matcher: WindowMatcher,
}, },
SetWindowMatcherAutoFocus {
matcher: WindowMatcher,
auto_focus: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -296,6 +296,16 @@ impl WindowCriterion<'_> {
pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) { pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
self.to_matcher().bind(cb); 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 { impl WindowMatcher {
@ -312,6 +322,14 @@ impl WindowMatcher {
pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) { pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
get!().set_window_matcher_handler(self, cb); 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 { impl MatchedWindow {

View file

@ -8,6 +8,7 @@ use {
config::handler::ConfigProxyHandler, config::handler::ConfigProxyHandler,
ifs::wl_seat::SeatId, ifs::wl_seat::SeatId,
state::State, state::State,
tree::ToplevelData,
utils::{ utils::{
clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt,
toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd,
@ -161,6 +162,13 @@ impl ConfigProxy {
handler.windows_to_tl_id.remove(&win); 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 { impl Drop for ConfigProxy {
@ -224,6 +232,7 @@ impl ConfigProxy {
window_matcher_cache: Default::default(), window_matcher_cache: Default::default(),
window_matcher_leafs: Default::default(), window_matcher_leafs: Default::default(),
window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW),
window_matcher_no_auto_focus: Default::default(),
}); });
let init_msg = bincode_ops() let init_msg = bincode_ops()
.serialize(&InitMessage::V1(V1InitMessage {})) .serialize(&InitMessage::V1(V1InitMessage {}))

View file

@ -124,6 +124,8 @@ pub(super) struct ConfigProxyHandler {
pub window_matcher_cache: CriterionCache<WindowCriterionIpc, ToplevelData>, pub window_matcher_cache: CriterionCache<WindowCriterionIpc, ToplevelData>,
pub window_matcher_leafs: CopyHashMap<WindowMatcher, Rc<TlmLeafMatcher>>, pub window_matcher_leafs: CopyHashMap<WindowMatcher, Rc<TlmLeafMatcher>>,
pub window_matcher_std_kinds: Rc<TlmUpstreamNode>, pub window_matcher_std_kinds: Rc<TlmUpstreamNode>,
pub window_matcher_no_auto_focus:
CopyHashMap<WindowMatcher, Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>>,
} }
pub struct Pollable { pub struct Pollable {
@ -2027,6 +2029,7 @@ impl ConfigProxyHandler {
fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) { fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) {
self.window_matchers.remove(&matcher); self.window_matchers.remove(&matcher);
self.window_matcher_leafs.remove(&matcher); self.window_matcher_leafs.remove(&matcher);
self.window_matcher_no_auto_focus.remove(&matcher);
} }
fn handle_enable_window_matcher_events( fn handle_enable_window_matcher_events(
@ -2056,6 +2059,20 @@ impl ConfigProxyHandler {
Ok(()) 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) { fn spaces_change(&self) {
struct V; struct V;
impl NodeVisitorBase for V { impl NodeVisitorBase for V {
@ -2861,9 +2878,24 @@ impl ConfigProxyHandler {
ClientMessage::EnableWindowMatcherEvents { matcher } => self ClientMessage::EnableWindowMatcherEvents { matcher } => self
.handle_enable_window_matcher_events(matcher) .handle_enable_window_matcher_events(matcher)
.wrn("enable_window_matcher_events")?, .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(()) 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)] #[derive(Debug, Error)]

View file

@ -665,11 +665,7 @@ impl State {
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) { pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
let seat = self.seat_queue.last(); let seat = self.seat_queue.last();
self.do_map_tiled(seat.as_deref(), node.clone()); self.do_map_tiled(seat.as_deref(), node.clone());
if node.node_visible() { self.focus_after_map(node, seat.as_deref());
if let Some(seat) = seat {
node.node_do_focus(&seat, Direction::Unspecified);
}
}
} }
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) { fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
@ -739,11 +735,22 @@ impl State {
Rect::new_sized(x1, y1, width, height).unwrap() Rect::new_sized(x1, y1, width, height).unwrap()
}; };
FloatNode::new(self, workspace, position, node.clone()); FloatNode::new(self, workspace, position, node.clone());
if node.node_visible() { self.focus_after_map(node, self.seat_queue.last().as_deref());
if let Some(seat) = self.seat_queue.last() { }
node.node_do_focus(&seat, Direction::Unspecified);
fn focus_after_map(&self, node: Rc<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
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<WlSeatGlobal>, name: &str) { pub fn show_workspace(&self, seat: &Rc<WlSeatGlobal>, name: &str) {

View file

@ -248,6 +248,7 @@ pub struct WindowRule {
pub match_: WindowMatch, pub match_: WindowMatch,
pub action: Option<Action>, pub action: Option<Action>,
pub latch: Option<Action>, pub latch: Option<Action>,
pub auto_focus: Option<bool>,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]

View file

@ -3,7 +3,7 @@ use {
config::{ config::{
WindowMatch, WindowRule, WindowMatch, WindowRule,
context::Context, context::Context,
extractor::{Extractor, ExtractorError, opt, str, val}, extractor::{Extractor, ExtractorError, bol, opt, recover, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{ parsers::{
action::{ActionParser, ActionParserError}, action::{ActionParser, ActionParserError},
@ -47,11 +47,12 @@ impl Parser for WindowRuleParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>, table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> { ) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table); 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(str("name")),
opt(val("match")), opt(val("match")),
opt(val("action")), opt(val("action")),
opt(val("latch")), opt(val("latch")),
recover(opt(bol("auto-focus"))),
))?; ))?;
let mut action = None; let mut action = None;
if let Some(value) = action_val { if let Some(value) = action_val {
@ -78,6 +79,7 @@ impl Parser for WindowRuleParser<'_> {
match_, match_,
action, action,
latch, latch,
auto_focus: auto_focus.despan(),
}) })
} }
} }

View file

@ -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> { fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> {

View file

@ -1904,6 +1904,10 @@
"latch": { "latch": {
"description": "An action to execute when a window no longer matches the criteria.", "description": "An action to execute when a window no longer matches the criteria.",
"$ref": "#/$defs/Action" "$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": [] "required": []

View file

@ -4203,6 +4203,15 @@ The table has the following fields:
The value of this field should be a [Action](#types-Action). 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.
<a name="types-WindowTypeMask"></a> <a name="types-WindowTypeMask"></a>
### `WindowTypeMask` ### `WindowTypeMask`

View file

@ -3368,6 +3368,14 @@ WindowRule:
ref: Action ref: Action
required: false required: false
description: An action to execute when a window no longer matches the criteria. 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: WindowMatch: