1
0
Fork 0
forked from wry/wry

config: add WM_CLASS window criteria

This commit is contained in:
Julian Orth 2025-05-03 13:37:23 +02:00
parent 6d3d4dcabb
commit faa0b27ef8
15 changed files with 159 additions and 10 deletions

View file

@ -124,4 +124,6 @@ pub enum WindowCriterionStringField {
Title, Title,
AppId, AppId,
Tag, Tag,
XClass,
XInstance,
} }

View file

@ -1665,6 +1665,10 @@ impl ConfigClient {
WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped,
WindowCriterion::Tag(t) => string!(t, Tag, false), WindowCriterion::Tag(t) => string!(t, Tag, false),
WindowCriterion::TagRegex(t) => string!(t, Tag, true), 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 }); let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion });
get_response!( get_response!(

View file

@ -264,6 +264,14 @@ pub enum WindowCriterion<'a> {
Tag(&'a str), Tag(&'a str),
/// Matches the toplevel-tag of the window with a regular expression. /// Matches the toplevel-tag of the window with a regular expression.
TagRegex(&'a str), 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<'_> { impl WindowCriterion<'_> {

View file

@ -1991,6 +1991,8 @@ impl ConfigProxyHandler {
WindowCriterionStringField::Title => mgr.title(needle), WindowCriterionStringField::Title => mgr.title(needle),
WindowCriterionStringField::AppId => mgr.app_id(needle), WindowCriterionStringField::AppId => mgr.app_id(needle),
WindowCriterionStringField::Tag => mgr.tag(needle), WindowCriterionStringField::Tag => mgr.tag(needle),
WindowCriterionStringField::XClass => mgr.class(needle),
WindowCriterionStringField::XInstance => mgr.instance(needle),
} }
} }
WindowCriterionIpc::Types(t) => mgr.kind(*t), WindowCriterionIpc::Types(t) => mgr.kind(*t),

View file

@ -18,7 +18,9 @@ use {
tlmm_just_mapped::TlmMatchJustMapped, tlmm_just_mapped::TlmMatchJustMapped,
tlmm_kind::TlmMatchKind, tlmm_kind::TlmMatchKind,
tlmm_seat_focus::TlmMatchSeatFocus, tlmm_seat_focus::TlmMatchSeatFocus,
tlmm_string::{TlmMatchAppId, TlmMatchTag, TlmMatchTitle}, tlmm_string::{
TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchTag, TlmMatchTitle,
},
tlmm_urgent::TlmMatchUrgent, tlmm_urgent::TlmMatchUrgent,
tlmm_visible::TlmMatchVisible, tlmm_visible::TlmMatchVisible,
}, },
@ -52,6 +54,7 @@ bitflags! {
TL_CHANGED_FULLSCREEN = 1 << 8, TL_CHANGED_FULLSCREEN = 1 << 8,
TL_CHANGED_JUST_MAPPED = 1 << 9, TL_CHANGED_JUST_MAPPED = 1 << 9,
TL_CHANGED_TAG = 1 << 10, TL_CHANGED_TAG = 1 << 10,
TL_CHANGED_CLASS_INST = 1 << 11,
} }
type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>; type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>;
@ -80,6 +83,8 @@ pub struct RootMatchers {
tag: TlmRootMatcherMap<TlmMatchTag>, tag: TlmRootMatcherMap<TlmMatchTag>,
app_id: TlmRootMatcherMap<TlmMatchAppId>, app_id: TlmRootMatcherMap<TlmMatchAppId>,
seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>, seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>,
class: TlmRootMatcherMap<TlmMatchClass>,
instance: TlmRootMatcherMap<TlmMatchInstance>,
} }
pub async fn handle_tl_changes(state: Rc<State>) { pub async fn handle_tl_changes(state: Rc<State>) {
@ -208,6 +213,8 @@ impl TlMatcherManager {
conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_APP_ID, app_id);
conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci);
conditional!(TL_CHANGED_TAG, tag); 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_FLOATING, floating);
fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_VISIBLE, visible);
fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_URGENT, urgent);
@ -281,6 +288,8 @@ impl TlMatcherManager {
conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_APP_ID, app_id);
conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci);
conditional!(TL_CHANGED_TAG, tag); 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_FLOATING, floating);
fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_VISIBLE, visible);
fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_URGENT, urgent);
@ -338,6 +347,14 @@ impl TlMatcherManager {
pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc<TlmUpstreamNode> { pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchSeatFocus::new(seat.id())) self.root(TlmMatchSeatFocus::new(seat.id()))
} }
pub fn class(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchClass::new(string))
}
pub fn instance(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchInstance::new(string))
}
} }
impl CritTarget for ToplevelData { impl CritTarget for ToplevelData {

View file

@ -11,10 +11,14 @@ pub type TlmMatchString<T> = CritMatchString<ToplevelData, T>;
pub type TlmMatchTitle = TlmMatchString<TitleAccess>; pub type TlmMatchTitle = TlmMatchString<TitleAccess>;
pub type TlmMatchAppId = TlmMatchString<AppIdAccess>; pub type TlmMatchAppId = TlmMatchString<AppIdAccess>;
pub type TlmMatchTag = TlmMatchString<TagAccess>; pub type TlmMatchTag = TlmMatchString<TagAccess>;
pub type TlmMatchClass = TlmMatchString<ClassAccess>;
pub type TlmMatchInstance = TlmMatchString<InstanceAccess>;
pub struct TitleAccess; pub struct TitleAccess;
pub struct AppIdAccess; pub struct AppIdAccess;
pub struct TagAccess; pub struct TagAccess;
pub struct ClassAccess;
pub struct InstanceAccess;
impl StringAccess<ToplevelData> for TitleAccess { impl StringAccess<ToplevelData> for TitleAccess {
fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool {
@ -48,3 +52,29 @@ impl StringAccess<ToplevelData> for TagAccess {
&roots.tag &roots.tag
} }
} }
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.class
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.instance
}
}

View file

@ -90,8 +90,8 @@ pub struct XwindowInfo {
pub override_redirect: Cell<bool>, pub override_redirect: Cell<bool>,
pub extents: Cell<Rect>, pub extents: Cell<Rect>,
pub pending_extents: Cell<Rect>, pub pending_extents: Cell<Rect>,
pub instance: RefCell<Option<BString>>, pub instance: RefCell<Option<String>>,
pub class: RefCell<Option<BString>>, pub class: RefCell<Option<String>>,
pub title: RefCell<Option<String>>, pub title: RefCell<Option<String>>,
pub role: RefCell<Option<BString>>, pub role: RefCell<Option<BString>>,
pub protocols: CopyHashMap<u32, ()>, pub protocols: CopyHashMap<u32, ()>,
@ -211,7 +211,7 @@ impl Xwindow {
&data.state, &data.state,
data.info.title.borrow_mut().clone().unwrap_or_default(), data.info.title.borrow_mut().clone().unwrap_or_default(),
Some(surface.client.clone()), Some(surface.client.clone()),
ToplevelType::XWindow, ToplevelType::XWindow(data.clone()),
id, id,
weak, weak,
); );

View file

@ -16,7 +16,10 @@ use {
jay_screencast::JayScreencast, jay_screencast::JayScreencast,
jay_toplevel::JayToplevel, jay_toplevel::JayToplevel,
wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, 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, rect::Rect,
state::State, state::State,
@ -273,12 +276,11 @@ impl ToplevelOpt {
} }
} }
#[derive(Debug)]
pub enum ToplevelType { pub enum ToplevelType {
Container, Container,
Placeholder, Placeholder,
XdgToplevel(Rc<XdgToplevelToplevelData>), XdgToplevel(Rc<XdgToplevelToplevelData>),
XWindow, XWindow(Rc<XwindowData>),
} }
impl ToplevelType { impl ToplevelType {
@ -287,7 +289,7 @@ impl ToplevelType {
ToplevelType::Container => window::CONTAINER, ToplevelType::Container => window::CONTAINER,
ToplevelType::Placeholder => window::PLACEHOLDER, ToplevelType::Placeholder => window::PLACEHOLDER,
ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL,
ToplevelType::XWindow => window::X_WINDOW, ToplevelType::XWindow { .. } => window::X_WINDOW,
} }
} }
} }

View file

@ -4,6 +4,7 @@ use {
crate::{ crate::{
async_engine::SpawnedFuture, async_engine::SpawnedFuture,
client::Client, client::Client,
criteria::tlm::TL_CHANGED_CLASS_INST,
ifs::{ ifs::{
ipc::{ ipc::{
DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable, DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable,
@ -1116,6 +1117,11 @@ impl Wm {
async fn load_window_wm_class(&self, data: &Rc<XwindowData>) { async fn load_window_wm_class(&self, data: &Rc<XwindowData>) {
let mut buf = vec![]; 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 match self
.c .c
.get_property::<u8>(data.window_id, ATOM_WM_CLASS, 0, &mut buf) .get_property::<u8>(data.window_id, ATOM_WM_CLASS, 0, &mut buf)
@ -1130,6 +1136,7 @@ impl Wm {
Err(XconError::PropertyUnavailable) => { Err(XconError::PropertyUnavailable) => {
data.info.instance.borrow_mut().take(); data.info.instance.borrow_mut().take();
data.info.class.borrow_mut().take(); data.info.class.borrow_mut().take();
property_changed();
return; return;
} }
Err(e) => { Err(e) => {
@ -1138,8 +1145,10 @@ impl Wm {
} }
} }
let mut iter = buf.split(|c| *c == 0); let mut iter = buf.split(|c| *c == 0);
*data.info.instance.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.class.borrow_mut() = Some(iter.next().unwrap_or(&[]).to_vec().into()); *data.info.instance.borrow_mut() = map();
*data.info.class.borrow_mut() = map();
property_changed();
} }
async fn load_window_wm_name2(&self, data: &Rc<XwindowData>, prop: u32, name: &str) { async fn load_window_wm_name2(&self, data: &Rc<XwindowData>, prop: u32, name: &str) {

View file

@ -267,6 +267,10 @@ pub struct WindowMatch {
pub just_mapped: Option<bool>, pub just_mapped: Option<bool>,
pub tag: Option<String>, pub tag: Option<String>,
pub tag_regex: Option<String>, pub tag_regex: Option<String>,
pub x_class: Option<String>,
pub x_class_regex: Option<String>,
pub x_instance: Option<String>,
pub x_instance_regex: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -68,6 +68,7 @@ impl Parser for WindowMatchParser<'_> {
tag, tag,
tag_regex, tag_regex,
), ),
(x_class, x_class_regex, x_instance, x_instance_regex),
) = ext.extract(( ) = ext.extract((
( (
opt(str("name")), opt(str("name")),
@ -92,6 +93,12 @@ impl Parser for WindowMatchParser<'_> {
opt(str("tag")), opt(str("tag")),
opt(str("tag-regex")), 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; let mut not = None;
if let Some(value) = not_val { if let Some(value) = not_val {
@ -144,6 +151,10 @@ impl Parser for WindowMatchParser<'_> {
just_mapped: just_mapped.despan(), just_mapped: just_mapped.despan(),
tag: tag.despan_into(), tag: tag.despan_into(),
tag_regex: tag_regex.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, types,
client, client,
}) })

View file

@ -260,6 +260,10 @@ impl Rule for WindowRule {
value!(AppIdRegex, app_id_regex); value!(AppIdRegex, app_id_regex);
value!(Tag, tag); value!(Tag, tag);
value!(TagRegex, tag_regex); 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!(Floating, floating);
bool!(Visible, visible); bool!(Visible, visible);
bool!(Urgent, urgent); bool!(Urgent, urgent);

View file

@ -1827,6 +1827,22 @@
"tag-regex": { "tag-regex": {
"type": "string", "type": "string",
"description": "Matches the toplevel-tag of the window with a regular expression." "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": [] "required": []

View file

@ -4085,6 +4085,30 @@ The table has the following fields:
The value of this field should be a string. 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.
<a name="types-WindowMatchExactly"></a> <a name="types-WindowMatchExactly"></a>
### `WindowMatchExactly` ### `WindowMatchExactly`

View file

@ -3519,6 +3519,22 @@ WindowMatch:
kind: string kind: string
required: false required: false
description: Matches the toplevel-tag of the window with a regular expression. 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: WindowMatchExactly: