From faa0b27ef8003ce550d6708d724e7fd702a9b377 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 13:37:23 +0200 Subject: [PATCH] config: add WM_CLASS window criteria --- jay-config/src/_private.rs | 2 ++ jay-config/src/_private/client.rs | 4 +++ jay-config/src/window.rs | 8 +++++ src/config/handler.rs | 2 ++ src/criteria/tlm.rs | 19 +++++++++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 30 +++++++++++++++++++ src/ifs/wl_surface/x_surface/xwindow.rs | 6 ++-- src/tree/toplevel.rs | 10 ++++--- src/xwayland/xwm.rs | 13 ++++++-- toml-config/src/config.rs | 4 +++ .../src/config/parsers/window_match.rs | 11 +++++++ toml-config/src/rules.rs | 4 +++ toml-spec/spec/spec.generated.json | 16 ++++++++++ toml-spec/spec/spec.generated.md | 24 +++++++++++++++ toml-spec/spec/spec.yaml | 16 ++++++++++ 15 files changed, 159 insertions(+), 10 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 881493e5..6d666c2f 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -124,4 +124,6 @@ pub enum WindowCriterionStringField { Title, AppId, Tag, + XClass, + XInstance, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 68fc1678..14cd901b 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1665,6 +1665,10 @@ impl ConfigClient { WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, WindowCriterion::Tag(t) => string!(t, Tag, false), WindowCriterion::TagRegex(t) => string!(t, Tag, true), + WindowCriterion::XClass(t) => string!(t, XClass, false), + WindowCriterion::XClassRegex(t) => string!(t, XClass, true), + WindowCriterion::XInstance(t) => string!(t, XInstance, false), + WindowCriterion::XInstanceRegex(t) => string!(t, XInstance, true), }; let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); get_response!( diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 3ee6bef9..e2438e21 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -264,6 +264,14 @@ pub enum WindowCriterion<'a> { Tag(&'a str), /// Matches the toplevel-tag of the window with a regular expression. TagRegex(&'a str), + /// Matches the X class of the window verbatim. + XClass(&'a str), + /// Matches the X class of the window with a regular expression. + XClassRegex(&'a str), + /// Matches the X instance of the window verbatim. + XInstance(&'a str), + /// Matches the X instance of the window with a regular expression. + XInstanceRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 0f17561f..6b9660f0 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1991,6 +1991,8 @@ impl ConfigProxyHandler { WindowCriterionStringField::Title => mgr.title(needle), WindowCriterionStringField::AppId => mgr.app_id(needle), WindowCriterionStringField::Tag => mgr.tag(needle), + WindowCriterionStringField::XClass => mgr.class(needle), + WindowCriterionStringField::XInstance => mgr.instance(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 042e245f..e58a51ed 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -18,7 +18,9 @@ use { tlmm_just_mapped::TlmMatchJustMapped, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, - tlmm_string::{TlmMatchAppId, TlmMatchTag, TlmMatchTitle}, + tlmm_string::{ + TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchTag, TlmMatchTitle, + }, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, @@ -52,6 +54,7 @@ bitflags! { TL_CHANGED_FULLSCREEN = 1 << 8, TL_CHANGED_JUST_MAPPED = 1 << 9, TL_CHANGED_TAG = 1 << 10, + TL_CHANGED_CLASS_INST = 1 << 11, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -80,6 +83,8 @@ pub struct RootMatchers { tag: TlmRootMatcherMap, app_id: TlmRootMatcherMap, seat_foci: TlmRootMatcherMap, + class: TlmRootMatcherMap, + instance: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -208,6 +213,8 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_TAG, tag); + conditional!(TL_CHANGED_CLASS_INST, class); + conditional!(TL_CHANGED_CLASS_INST, instance); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -281,6 +288,8 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_TAG, tag); + conditional!(TL_CHANGED_CLASS_INST, class); + conditional!(TL_CHANGED_CLASS_INST, instance); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -338,6 +347,14 @@ impl TlMatcherManager { pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { self.root(TlmMatchSeatFocus::new(seat.id())) } + + pub fn class(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchClass::new(string)) + } + + pub fn instance(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchInstance::new(string)) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index 352c08b4..6800ecdf 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -11,10 +11,14 @@ pub type TlmMatchString = CritMatchString; pub type TlmMatchTitle = TlmMatchString; pub type TlmMatchAppId = TlmMatchString; pub type TlmMatchTag = TlmMatchString; +pub type TlmMatchClass = TlmMatchString; +pub type TlmMatchInstance = TlmMatchString; pub struct TitleAccess; pub struct AppIdAccess; pub struct TagAccess; +pub struct ClassAccess; +pub struct InstanceAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -48,3 +52,29 @@ impl StringAccess for TagAccess { &roots.tag } } + +impl StringAccess for ClassAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XWindow(data) = &data.kind { + return f(&data.info.class.borrow().as_deref().unwrap_or_default()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.class + } +} + +impl StringAccess for InstanceAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XWindow(data) = &data.kind { + return f(&data.info.instance.borrow().as_deref().unwrap_or_default()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.instance + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index b6ca97be..e0f497be 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -90,8 +90,8 @@ pub struct XwindowInfo { pub override_redirect: Cell, pub extents: Cell, pub pending_extents: Cell, - pub instance: RefCell>, - pub class: RefCell>, + pub instance: RefCell>, + pub class: RefCell>, pub title: RefCell>, pub role: RefCell>, pub protocols: CopyHashMap, @@ -211,7 +211,7 @@ impl Xwindow { &data.state, data.info.title.borrow_mut().clone().unwrap_or_default(), Some(surface.client.clone()), - ToplevelType::XWindow, + ToplevelType::XWindow(data.clone()), id, weak, ); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index dd8daa83..6992aac6 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -16,7 +16,10 @@ use { jay_screencast::JayScreencast, jay_toplevel::JayToplevel, wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, - wl_surface::{WlSurface, xdg_surface::xdg_toplevel::XdgToplevelToplevelData}, + wl_surface::{ + WlSurface, x_surface::xwindow::XwindowData, + xdg_surface::xdg_toplevel::XdgToplevelToplevelData, + }, }, rect::Rect, state::State, @@ -273,12 +276,11 @@ impl ToplevelOpt { } } -#[derive(Debug)] pub enum ToplevelType { Container, Placeholder, XdgToplevel(Rc), - XWindow, + XWindow(Rc), } impl ToplevelType { @@ -287,7 +289,7 @@ impl ToplevelType { ToplevelType::Container => window::CONTAINER, ToplevelType::Placeholder => window::PLACEHOLDER, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, - ToplevelType::XWindow => window::X_WINDOW, + ToplevelType::XWindow { .. } => window::X_WINDOW, } } } diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index e809f8b3..8f1d27b9 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -4,6 +4,7 @@ use { crate::{ async_engine::SpawnedFuture, client::Client, + criteria::tlm::TL_CHANGED_CLASS_INST, ifs::{ ipc::{ DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable, @@ -1116,6 +1117,11 @@ impl Wm { async fn load_window_wm_class(&self, data: &Rc) { let mut buf = vec![]; + let property_changed = || { + if let Some(window) = data.window.get() { + window.toplevel_data.property_changed(TL_CHANGED_CLASS_INST); + } + }; match self .c .get_property::(data.window_id, ATOM_WM_CLASS, 0, &mut buf) @@ -1130,6 +1136,7 @@ impl Wm { Err(XconError::PropertyUnavailable) => { data.info.instance.borrow_mut().take(); data.info.class.borrow_mut().take(); + property_changed(); return; } Err(e) => { @@ -1138,8 +1145,10 @@ impl Wm { } } let mut iter = buf.split(|c| *c == 0); - *data.info.instance.borrow_mut() = Some(iter.next().unwrap_or(&[]).to_vec().into()); - *data.info.class.borrow_mut() = Some(iter.next().unwrap_or(&[]).to_vec().into()); + let mut map = || Some(iter.next().unwrap_or(&[]).to_str_lossy().into_owned()); + *data.info.instance.borrow_mut() = map(); + *data.info.class.borrow_mut() = map(); + property_changed(); } async fn load_window_wm_name2(&self, data: &Rc, prop: u32, name: &str) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index bd95a0e3..04076390 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -267,6 +267,10 @@ pub struct WindowMatch { pub just_mapped: Option, pub tag: Option, pub tag_regex: Option, + pub x_class: Option, + pub x_class_regex: Option, + pub x_instance: Option, + pub x_instance_regex: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/window_match.rs b/toml-config/src/config/parsers/window_match.rs index f0f3eba8..e2d5ef2b 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -68,6 +68,7 @@ impl Parser for WindowMatchParser<'_> { tag, tag_regex, ), + (x_class, x_class_regex, x_instance, x_instance_regex), ) = ext.extract(( ( opt(str("name")), @@ -92,6 +93,12 @@ impl Parser for WindowMatchParser<'_> { opt(str("tag")), opt(str("tag-regex")), ), + ( + opt(str("x-class")), + opt(str("x-class-regex")), + opt(str("x-instance")), + opt(str("x-instance-regex")), + ), ))?; let mut not = None; if let Some(value) = not_val { @@ -144,6 +151,10 @@ impl Parser for WindowMatchParser<'_> { just_mapped: just_mapped.despan(), tag: tag.despan_into(), tag_regex: tag_regex.despan_into(), + x_class: x_class.despan_into(), + x_class_regex: x_class_regex.despan_into(), + x_instance: x_instance.despan_into(), + x_instance_regex: x_instance_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index c58f0f26..e03c03b9 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -260,6 +260,10 @@ impl Rule for WindowRule { value!(AppIdRegex, app_id_regex); value!(Tag, tag); value!(TagRegex, tag_regex); + value!(XClass, x_class); + value!(XClassRegex, x_class_regex); + value!(XInstance, x_instance); + value!(XInstanceRegex, x_instance_regex); bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1bb7c8a8..72c6947d 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1827,6 +1827,22 @@ "tag-regex": { "type": "string", "description": "Matches the toplevel-tag of the window with a regular expression." + }, + "x-class": { + "type": "string", + "description": "Matches the X class of the window verbatim." + }, + "x-class-regex": { + "type": "string", + "description": "Matches the X class of the window with a regular expression." + }, + "x-instance": { + "type": "string", + "description": "Matches the X instance of the window verbatim." + }, + "x-instance-regex": { + "type": "string", + "description": "Matches the X instance of the window with a regular expression." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index e5f0e8a4..90d80aca 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4085,6 +4085,30 @@ The table has the following fields: The value of this field should be a string. +- `x-class` (optional): + + Matches the X class of the window verbatim. + + The value of this field should be a string. + +- `x-class-regex` (optional): + + Matches the X class of the window with a regular expression. + + The value of this field should be a string. + +- `x-instance` (optional): + + Matches the X instance of the window verbatim. + + The value of this field should be a string. + +- `x-instance-regex` (optional): + + Matches the X instance of the window with a regular expression. + + The value of this field should be a string. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 134dba12..e3f76683 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3519,6 +3519,22 @@ WindowMatch: kind: string required: false description: Matches the toplevel-tag of the window with a regular expression. + x-class: + kind: string + required: false + description: Matches the X class of the window verbatim. + x-class-regex: + kind: string + required: false + description: Matches the X class of the window with a regular expression. + x-instance: + kind: string + required: false + description: Matches the X instance of the window verbatim. + x-instance-regex: + kind: string + required: false + description: Matches the X instance of the window with a regular expression. WindowMatchExactly: