diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 2aa0c2ac..2845a2bc 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -109,6 +109,7 @@ pub enum WindowCriterionIpc { regex: bool, }, Types(WindowType), + Client(ClientMatcher), } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index a1a7bc4c..77cb47d2 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1638,6 +1638,7 @@ impl ConfigClient { destroy_matcher, ) }; + let _destroy_client_matcher; let criterion = match criterion { WindowCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)), WindowCriterion::Not(c) => return generic(GenericCriterion::Not(c)), @@ -1645,6 +1646,13 @@ impl ConfigClient { WindowCriterion::Any(c) => return generic(GenericCriterion::Any(c)), WindowCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)), WindowCriterion::Types(t) => WindowCriterionIpc::Types(t), + WindowCriterion::Client(c) => { + let (matcher, original) = self.create_client_matcher_(*c, true); + if original { + _destroy_client_matcher = on_drop(move || matcher.destroy()); + } + WindowCriterionIpc::Client(matcher) + } }; 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 3205e36c..e8aeec2a 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -1,7 +1,10 @@ //! Tools for inspecting and manipulating windows. use { - crate::{Axis, Direction, Workspace, client::Client}, + crate::{ + Axis, Direction, Workspace, + client::{Client, ClientCriterion}, + }, serde::{Deserialize, Serialize}, std::ops::Deref, }; @@ -231,6 +234,8 @@ pub enum WindowCriterion<'a> { Any(&'a [WindowCriterion<'a>]), /// Matches if an exact number of the contained criteria match. Exactly(usize, &'a [WindowCriterion<'a>]), + /// Matches if the window's client matches the client criterion. + Client(&'a ClientCriterion<'a>), } impl WindowCriterion<'_> { diff --git a/src/compositor.rs b/src/compositor.rs index 1a2627d1..2c3352ee 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -230,6 +230,7 @@ fn start_compositor2( ipc_device_ids: Default::default(), use_wire_scale: Default::default(), wire_scale: Default::default(), + windows: Default::default(), }, acceptor: Default::default(), serial: Default::default(), diff --git a/src/config/handler.rs b/src/config/handler.rs index b2adf0e9..abc51d60 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1991,6 +1991,10 @@ impl ConfigProxyHandler { match *field {} } WindowCriterionIpc::Types(t) => mgr.kind(*t), + WindowCriterionIpc::Client(c) => { + self.state.cl_matcher_manager.rematch_all(&self.state); + mgr.client(&self.state, &self.get_client_matcher(*c)?.node) + } }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/crit_graph.rs b/src/criteria/crit_graph.rs index e7ed8be4..44d609a1 100644 --- a/src/criteria/crit_graph.rs +++ b/src/criteria/crit_graph.rs @@ -12,5 +12,7 @@ pub use { CritRootFixed, }, crit_target::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, - crit_upstream::{CritUpstreamData, CritUpstreamNode}, + crit_upstream::{ + CritUpstreamData, CritUpstreamNode, CritUpstreamNodeBase, CritUpstreamNodeData, + }, }; diff --git a/src/criteria/crit_graph/crit_upstream.rs b/src/criteria/crit_graph/crit_upstream.rs index 5042e9a5..7e21c78e 100644 --- a/src/criteria/crit_graph/crit_upstream.rs +++ b/src/criteria/crit_graph/crit_upstream.rs @@ -55,7 +55,6 @@ where fn detach(&self, id: CritMatcherId); fn not(&self, mgr: &Target::Mgr) -> Rc>; fn pull(&self, target: &Target) -> bool; - #[expect(dead_code)] fn get(&self, target: &Target) -> bool; } diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 376af00f..96581ece 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -5,10 +5,11 @@ use { criteria::{ CritDestroyListener, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, + clm::ClmUpstreamNode, crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, crit_leaf::{CritLeafEvent, CritLeafMatcher}, crit_matchers::critm_constant::CritMatchConstant, - tlm::tlm_matchers::tlmm_kind::TlmMatchKind, + tlm::tlm_matchers::{tlmm_client::TlmMatchClient, tlmm_kind::TlmMatchKind}, }, state::State, tree::{NodeId, ToplevelData, ToplevelNode}, @@ -42,6 +43,7 @@ type TlmRootMatcherMap = RootMatcherMap; #[derive(Default)] pub struct RootMatchers { kinds: TlmRootMatcherMap, + clients: CopyHashMap>, } pub async fn handle_tl_changes(state: Rc) { @@ -115,6 +117,7 @@ impl TlMatcherManager { }; } unconditional!(kinds); + unconditional!(clients); if self.constant[true].has_downstream() { return true; } @@ -182,6 +185,7 @@ impl TlMatcherManager { }; } unconditional!(kinds); + unconditional!(clients); self.constant[true].handle(data); } #[expect(unused_macros)] @@ -207,6 +211,10 @@ impl TlMatcherManager { pub fn kind(&self, kind: WindowType) -> Rc { self.root(TlmMatchKind::new(kind)) } + + pub fn client(&self, state: &Rc, client: &Rc) -> Rc { + TlmMatchClient::new(state, client) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 93871e79..4348b709 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -18,4 +18,5 @@ macro_rules! fixed_root_criterion { }; } +pub mod tlmm_client; pub mod tlmm_kind; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_client.rs b/src/criteria/tlm/tlm_matchers/tlmm_client.rs new file mode 100644 index 00000000..f9dc83ad --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_client.rs @@ -0,0 +1,117 @@ +use { + crate::{ + client::Client, + criteria::{ + CritMatcherId, CritUpstreamNode, + clm::ClmUpstreamNode, + crit_graph::{ + CritDownstream, CritDownstreamData, CritMgr, CritUpstreamData, + CritUpstreamNodeBase, CritUpstreamNodeData, + }, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, + tlm::TlMatcherManager, + }, + state::State, + tree::{ToplevelData, ToplevelNodeBase}, + }, + std::rc::Rc, +}; + +pub struct TlmMatchClient { + id: CritMatcherId, + state: Rc, + node: Rc, + upstream: CritDownstreamData>, + downstream: CritUpstreamData, +} + +impl TlmMatchClient { + pub fn new(state: &Rc, node: &Rc) -> Rc { + let id = state.tl_matcher_manager.id(); + let slf = Rc::new_cyclic(|slf| Self { + id, + state: state.clone(), + node: node.clone(), + upstream: CritDownstreamData::new(id, &[node.clone()]), + downstream: CritUpstreamData::new(slf, id), + }); + slf.upstream.attach(&slf); + state + .tl_matcher_manager + .matchers + .clients + .set(id, Rc::downgrade(&slf)); + slf + } + + pub fn handle(&self, node: &ToplevelData) { + if let Some(client) = &node.client { + if self.node.get(client) { + let data = self.downstream.get_or_create(node); + self.downstream.update_matched(node, data, true, false); + } + } + } +} + +impl CritUpstreamNodeBase for TlmMatchClient { + type Data = (); + + fn data(&self) -> &CritUpstreamData { + &self.downstream + } + + fn not(&self, _mgr: &TlMatcherManager) -> Rc> { + Self::new(&self.state, &self.node.not(&self.state.cl_matcher_manager)) + } + + fn pull(&self, target: &ToplevelData) -> bool { + if let Some(client) = &target.client { + return self.node.pull(client); + } + false + } +} + +impl CritDownstream> for TlmMatchClient { + fn update_matched(self: Rc, target: &Rc, matched: bool) { + let handle = |data: &ToplevelData| { + let node = match matched { + true => self.downstream.get_or_create(data), + false => match self.downstream.get(data) { + Some(n) => n, + None => return, + }, + }; + self.downstream + .update_matched(data, node, matched, !matched); + }; + if target.is_xwayland { + for tl in self.state.xwayland.windows.lock().values() { + handle(tl.tl_data()); + } + } else { + for tl in target.objects.xdg_toplevel.lock().values() { + handle(tl.tl_data()); + } + } + } +} + +impl CritDestroyListenerBase for TlmMatchClient { + type Data = CritUpstreamNodeData; + + fn data(&self) -> &CritPerTargetData { + &self.downstream.nodes + } +} + +impl Drop for TlmMatchClient { + fn drop(&mut self) { + self.state + .tl_matcher_manager + .matchers + .clients + .remove(&self.id); + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 1c6664c2..b6ca97be 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -238,6 +238,13 @@ impl Xwindow { self.tl_destroy(); self.x.surface.set_toplevel(None); self.x.xwindow.set(None); + self.x + .surface + .client + .state + .xwayland + .windows + .remove(&self.id); } pub fn is_mapped(&self) -> bool { diff --git a/src/state.rs b/src/state.rs index b0dd776f..f870ac9e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -57,6 +57,7 @@ use { NoneSurfaceExt, tray::TrayItemIds, wl_subsurface::SubsurfaceIds, + x_surface::xwindow::{Xwindow, XwindowId}, zwp_idle_inhibitor_v1::{IdleInhibitorId, IdleInhibitorIds, ZwpIdleInhibitorV1}, zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, }, @@ -271,6 +272,7 @@ pub struct XWaylandState { pub ipc_device_ids: XIpcDeviceIds, pub use_wire_scale: Cell, pub wire_scale: Cell>, + pub windows: CopyHashMap>, } pub struct IdleState { diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index 1d5f0b33..e809f8b3 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -1459,6 +1459,7 @@ impl Wm { return; } }; + self.state.xwayland.windows.set(window.id, window.clone()); data.window.set(Some(window.clone())); { self.load_window_wm_class(data).await; diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 2360fc77..40bd2e4d 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -254,6 +254,7 @@ pub struct WindowRule { pub struct WindowMatch { pub generic: GenericMatch, pub types: Option, + pub client: 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 3c41403d..4836a48b 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -5,7 +5,10 @@ use { context::Context, extractor::{Extractor, ExtractorError, arr, n32, opt, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, - parsers::window_type::{WindowTypeParser, WindowTypeParserError}, + parsers::{ + client_match::{ClientMatchParser, ClientMatchParserError}, + window_type::{WindowTypeParser, WindowTypeParserError}, + }, }, toml::{ toml_span::{DespanExt, Span, Spanned}, @@ -24,6 +27,8 @@ pub enum WindowMatchParserError { Extract(#[from] ExtractorError), #[error(transparent)] WindowTypes(#[from] WindowTypeParserError), + #[error(transparent)] + ClientMatchParserError(#[from] ClientMatchParserError), } pub struct WindowMatchParser<'a>(pub &'a Context<'a>); @@ -39,14 +44,16 @@ impl Parser for WindowMatchParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let ((name, not_val, all_val, any_val, exactly_val, types_val),) = ext.extract((( - opt(str("name")), - opt(val("not")), - opt(arr("all")), - opt(arr("any")), - opt(val("exactly")), - opt(val("types")), - ),))?; + let ((name, not_val, all_val, any_val, exactly_val, types_val, client_val),) = ext + .extract((( + opt(str("name")), + opt(val("not")), + opt(arr("all")), + opt(arr("any")), + opt(val("exactly")), + opt(val("types")), + opt(val("client")), + ),))?; let mut not = None; if let Some(value) = not_val { not = Some(Box::new(value.parse(&mut WindowMatchParser(self.0))?)); @@ -74,6 +81,10 @@ impl Parser for WindowMatchParser<'_> { if let Some(value) = exactly_val { exactly = Some(value.parse(&mut WindowMatchExactlyParser(self.0))?); } + let mut client = None; + if let Some(value) = client_val { + client = Some(value.parse_map(&mut ClientMatchParser(self.0))?); + } Ok(WindowMatch { generic: GenericMatch { name: name.despan_into(), @@ -83,6 +94,7 @@ impl Parser for WindowMatchParser<'_> { exactly, }, types, + client, }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 7b5d256a..d7cf586b 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -219,7 +219,7 @@ impl Rule for WindowRule { } fn map_custom( - _state: &Rc, + state: &Rc, all: &mut Vec>, match_: &Self::Match, ) -> Option<()> { @@ -248,6 +248,14 @@ impl Rule for WindowRule { if let Some(value) = &match_.types { all.push(m(WindowCriterion::Types(*value))); } + if let Some(value) = &match_.client { + let mut mapper = state.persistent.client_rule_mapper.borrow_mut(); + let mapper = mapper.as_mut()?; + let matcher = mapper.map_temporary_match(&[], value)?; + all.push(m(WindowCriterion::Client(&ClientCriterion::Matcher( + matcher.0, + )))); + } Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1b562e07..79ab0227 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1775,6 +1775,10 @@ "types": { "description": "Matches windows whose type is contained in the mask.", "$ref": "#/$defs/WindowTypeMask" + }, + "client": { + "description": "Matches if the window's client matches the client criterion.", + "$ref": "#/$defs/ClientMatch" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 97245dfd..0b96cbf0 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4004,6 +4004,12 @@ The table has the following fields: The value of this field should be a [WindowTypeMask](#types-WindowTypeMask). +- `client` (optional): + + Matches if the window's client matches the client criterion. + + The value of this field should be a [ClientMatch](#types-ClientMatch). + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index c243a6aa..2f1f2ea3 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3463,6 +3463,10 @@ WindowMatch: ref: WindowTypeMask required: false description: Matches windows whose type is contained in the mask. + client: + ref: ClientMatch + required: false + description: Matches if the window's client matches the client criterion. WindowMatchExactly: