1
0
Fork 0
forked from wry/wry

config: add toplevel-tag window criteria

This commit is contained in:
Julian Orth 2025-05-01 17:31:42 +02:00
parent 5f1268cada
commit 6d3d4dcabb
15 changed files with 106 additions and 8 deletions

View file

@ -123,4 +123,5 @@ pub enum WindowCriterionIpc {
pub enum WindowCriterionStringField { pub enum WindowCriterionStringField {
Title, Title,
AppId, AppId,
Tag,
} }

View file

@ -1663,6 +1663,8 @@ impl ConfigClient {
WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat),
WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen,
WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped,
WindowCriterion::Tag(t) => string!(t, Tag, false),
WindowCriterion::TagRegex(t) => string!(t, Tag, 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

@ -260,6 +260,10 @@ pub enum WindowCriterion<'a> {
/// This is true for one iteration of the compositor's main loop immediately after the /// This is true for one iteration of the compositor's main loop immediately after the
/// window has been mapped. /// window has been mapped.
JustMapped, JustMapped,
/// Matches the toplevel-tag of the window verbatim.
Tag(&'a str),
/// Matches the toplevel-tag of the window with a regular expression.
TagRegex(&'a str),
} }
impl WindowCriterion<'_> { impl WindowCriterion<'_> {

View file

@ -1990,6 +1990,7 @@ impl ConfigProxyHandler {
match *field { match *field {
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),
} }
} }
WindowCriterionIpc::Types(t) => mgr.kind(*t), WindowCriterionIpc::Types(t) => mgr.kind(*t),

View file

@ -18,7 +18,7 @@ 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, TlmMatchTitle}, tlmm_string::{TlmMatchAppId, TlmMatchTag, TlmMatchTitle},
tlmm_urgent::TlmMatchUrgent, tlmm_urgent::TlmMatchUrgent,
tlmm_visible::TlmMatchVisible, tlmm_visible::TlmMatchVisible,
}, },
@ -51,6 +51,7 @@ bitflags! {
TL_CHANGED_SEAT_FOCI = 1 << 7, TL_CHANGED_SEAT_FOCI = 1 << 7,
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,
} }
type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>; type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>;
@ -76,6 +77,7 @@ pub struct RootMatchers {
kinds: TlmRootMatcherMap<TlmMatchKind>, kinds: TlmRootMatcherMap<TlmMatchKind>,
clients: CopyHashMap<CritMatcherId, Weak<TlmMatchClient>>, clients: CopyHashMap<CritMatcherId, Weak<TlmMatchClient>>,
title: TlmRootMatcherMap<TlmMatchTitle>, title: TlmRootMatcherMap<TlmMatchTitle>,
tag: TlmRootMatcherMap<TlmMatchTag>,
app_id: TlmRootMatcherMap<TlmMatchAppId>, app_id: TlmRootMatcherMap<TlmMatchAppId>,
seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>, seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>,
} }
@ -205,6 +207,7 @@ impl TlMatcherManager {
conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_TITLE, title);
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);
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);
@ -277,6 +280,7 @@ impl TlMatcherManager {
conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_TITLE, title);
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);
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);
@ -299,6 +303,10 @@ impl TlMatcherManager {
self.root(TlmMatchAppId::new(string)) self.root(TlmMatchAppId::new(string))
} }
pub fn tag(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchTag::new(string))
}
pub fn floating(&self) -> Rc<TlmUpstreamNode> { pub fn floating(&self) -> Rc<TlmUpstreamNode> {
self.floating[true].clone() self.floating[true].clone()
} }

View file

@ -3,16 +3,18 @@ use crate::{
crit_matchers::critm_string::{CritMatchString, StringAccess}, crit_matchers::critm_string::{CritMatchString, StringAccess},
tlm::{RootMatchers, TlmRootMatcherMap}, tlm::{RootMatchers, TlmRootMatcherMap},
}, },
tree::ToplevelData, tree::{ToplevelData, ToplevelType},
}; };
pub type TlmMatchString<T> = CritMatchString<ToplevelData, T>; 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 struct TitleAccess; pub struct TitleAccess;
pub struct AppIdAccess; pub struct AppIdAccess;
pub struct TagAccess;
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 {
@ -33,3 +35,16 @@ impl StringAccess<ToplevelData> for AppIdAccess {
&roots.app_id &roots.app_id
} }
} }
impl StringAccess<ToplevelData> for TagAccess {
fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool {
if let ToplevelType::XdgToplevel(data) = &data.kind {
return f(&data.tag.borrow());
}
false
}
fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap<TlmMatchString<Self>> {
&roots.tag
}
}

View file

@ -92,6 +92,11 @@ pub enum Decoration {
Server, Server,
} }
#[derive(Debug)]
pub struct XdgToplevelToplevelData {
pub tag: RefCell<String>,
}
pub struct XdgToplevel { pub struct XdgToplevel {
pub id: XdgToplevelId, pub id: XdgToplevelId,
pub state: Rc<State>, pub state: Rc<State>,
@ -112,6 +117,7 @@ pub struct XdgToplevel {
is_mapped: Cell<bool>, is_mapped: Cell<bool>,
dialog: CloneCell<Option<Rc<XdgDialogV1>>>, dialog: CloneCell<Option<Rc<XdgDialogV1>>>,
extents_set: Cell<bool>, extents_set: Cell<bool>,
pub data: Rc<XdgToplevelToplevelData>,
} }
impl Debug for XdgToplevel { impl Debug for XdgToplevel {
@ -135,6 +141,9 @@ impl XdgToplevel {
} }
let state = &surface.surface.client.state; let state = &surface.surface.client.state;
let node_id = state.node_ids.next(); let node_id = state.node_ids.next();
let data = Rc::new(XdgToplevelToplevelData {
tag: Default::default(),
});
Self { Self {
id, id,
state: state.clone(), state: state.clone(),
@ -154,7 +163,7 @@ impl XdgToplevel {
state, state,
String::new(), String::new(),
Some(surface.surface.client.clone()), Some(surface.surface.client.clone()),
ToplevelType::XdgToplevel, ToplevelType::XdgToplevel(data.clone()),
node_id, node_id,
slf, slf,
), ),
@ -162,6 +171,7 @@ impl XdgToplevel {
is_mapped: Cell::new(false), is_mapped: Cell::new(false),
dialog: Default::default(), dialog: Default::default(),
extents_set: Cell::new(false), extents_set: Cell::new(false),
data,
} }
} }

View file

@ -1,9 +1,11 @@
use { use {
crate::{ crate::{
client::{Client, ClientError}, client::{Client, ClientError},
criteria::tlm::TL_CHANGED_TAG,
globals::{Global, GlobalName}, globals::{Global, GlobalName},
leaks::Tracker, leaks::Tracker,
object::{Object, Version}, object::{Object, Version},
tree::ToplevelNodeBase,
wire::{XdgToplevelTagManagerV1Id, xdg_toplevel_tag_manager_v1::*}, wire::{XdgToplevelTagManagerV1Id, xdg_toplevel_tag_manager_v1::*},
}, },
std::rc::Rc, std::rc::Rc,
@ -72,9 +74,17 @@ impl XdgToplevelTagManagerV1RequestHandler for XdgToplevelTagManagerV1 {
fn set_toplevel_tag( fn set_toplevel_tag(
&self, &self,
_req: SetToplevelTag<'_>, req: SetToplevelTag<'_>,
_slf: &Rc<Self>, _slf: &Rc<Self>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
let tl = self.client.lookup(req.toplevel)?;
let tag = &mut *tl.data.tag.borrow_mut();
if tag == req.tag {
return Ok(());
}
tag.clear();
tag.push_str(req.tag);
tl.tl_data().property_changed(TL_CHANGED_TAG);
Ok(()) Ok(())
} }

View file

@ -16,7 +16,7 @@ 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, wl_surface::{WlSurface, xdg_surface::xdg_toplevel::XdgToplevelToplevelData},
}, },
rect::Rect, rect::Rect,
state::State, state::State,
@ -277,7 +277,7 @@ impl ToplevelOpt {
pub enum ToplevelType { pub enum ToplevelType {
Container, Container,
Placeholder, Placeholder,
XdgToplevel, XdgToplevel(Rc<XdgToplevelToplevelData>),
XWindow, XWindow,
} }
@ -286,7 +286,7 @@ impl ToplevelType {
match self { match self {
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

@ -265,6 +265,8 @@ pub struct WindowMatch {
pub focused: Option<bool>, pub focused: Option<bool>,
pub fullscreen: Option<bool>, pub fullscreen: Option<bool>,
pub just_mapped: Option<bool>, pub just_mapped: Option<bool>,
pub tag: Option<String>,
pub tag_regex: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -56,7 +56,18 @@ impl Parser for WindowMatchParser<'_> {
title, title,
title_regex, title_regex,
), ),
(app_id, app_id_regex, floating, visible, urgent, focused, fullscreen, just_mapped), (
app_id,
app_id_regex,
floating,
visible,
urgent,
focused,
fullscreen,
just_mapped,
tag,
tag_regex,
),
) = ext.extract(( ) = ext.extract((
( (
opt(str("name")), opt(str("name")),
@ -78,6 +89,8 @@ impl Parser for WindowMatchParser<'_> {
opt(bol("focused")), opt(bol("focused")),
opt(bol("fullscreen")), opt(bol("fullscreen")),
opt(bol("just-mapped")), opt(bol("just-mapped")),
opt(str("tag")),
opt(str("tag-regex")),
), ),
))?; ))?;
let mut not = None; let mut not = None;
@ -129,6 +142,8 @@ impl Parser for WindowMatchParser<'_> {
focused: focused.despan(), focused: focused.despan(),
fullscreen: fullscreen.despan(), fullscreen: fullscreen.despan(),
just_mapped: just_mapped.despan(), just_mapped: just_mapped.despan(),
tag: tag.despan_into(),
tag_regex: tag_regex.despan_into(),
types, types,
client, client,
}) })

View file

@ -258,6 +258,8 @@ impl Rule for WindowRule {
value!(TitleRegex, title_regex); value!(TitleRegex, title_regex);
value!(AppId, app_id); value!(AppId, app_id);
value!(AppIdRegex, app_id_regex); value!(AppIdRegex, app_id_regex);
value!(Tag, tag);
value!(TagRegex, tag_regex);
bool!(Floating, floating); bool!(Floating, floating);
bool!(Visible, visible); bool!(Visible, visible);
bool!(Urgent, urgent); bool!(Urgent, urgent);

View file

@ -1819,6 +1819,14 @@
"just-mapped": { "just-mapped": {
"type": "boolean", "type": "boolean",
"description": "Matches if the window has/hasn't just been mapped.\n\nThis is true for one iteration of the compositor's main loop immediately after the\nwindow has been mapped.\n" "description": "Matches if the window has/hasn't just been mapped.\n\nThis is true for one iteration of the compositor's main loop immediately after the\nwindow has been mapped.\n"
},
"tag": {
"type": "string",
"description": "Matches the toplevel-tag of the window verbatim."
},
"tag-regex": {
"type": "string",
"description": "Matches the toplevel-tag of the window with a regular expression."
} }
}, },
"required": [] "required": []

View file

@ -4073,6 +4073,18 @@ The table has the following fields:
The value of this field should be a boolean. The value of this field should be a boolean.
- `tag` (optional):
Matches the toplevel-tag of the window verbatim.
The value of this field should be a string.
- `tag-regex` (optional):
Matches the toplevel-tag 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

@ -3511,6 +3511,14 @@ WindowMatch:
This is true for one iteration of the compositor's main loop immediately after the This is true for one iteration of the compositor's main loop immediately after the
window has been mapped. window has been mapped.
tag:
kind: string
required: false
description: Matches the toplevel-tag of the window verbatim.
tag-regex:
kind: string
required: false
description: Matches the toplevel-tag of the window with a regular expression.
WindowMatchExactly: WindowMatchExactly: