From 8b19315f509633733e5cea7a52268e40a41e5efb Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 27 Feb 2026 20:53:09 +0100 Subject: [PATCH] config: allow matching on client tag --- docs/window-and-client-rules.md | 1 + jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 2 ++ jay-config/src/client.rs | 4 ++++ src/config/handler.rs | 1 + src/criteria/clm.rs | 9 ++++++- src/criteria/clm/clm_matchers/clmm_string.rs | 24 +++++++++++++++---- toml-config/src/config.rs | 2 ++ .../src/config/parsers/client_match.rs | 10 ++++++-- toml-config/src/rules.rs | 2 ++ toml-spec/spec/spec.generated.json | 8 +++++++ toml-spec/spec/spec.generated.md | 12 ++++++++++ toml-spec/spec/spec.yaml | 8 +++++++ 13 files changed, 76 insertions(+), 8 deletions(-) diff --git a/docs/window-and-client-rules.md b/docs/window-and-client-rules.md index 88a9590b..6d4f846d 100644 --- a/docs/window-and-client-rules.md +++ b/docs/window-and-client-rules.md @@ -204,6 +204,7 @@ The full specification of client criteria can be found in - `is-xwayland` - Matches if the client is/isn't Xwayland. - `comm`, `comm-regex` - Matches the `/proc/self/comm` of the client. - `exe`, `exe-regex` - Matches the `/proc/self/exe` of the client. +- `tag`, `tag-regex` - Matches the tag of the client. ## Window Rules diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 45034dd0..facb7c17 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -99,6 +99,7 @@ pub enum ClientCriterionStringField { SandboxInstanceId, Comm, Exe, + Tag, } #[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 e4cfbd0d..651c57d2 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1764,6 +1764,8 @@ impl ConfigClient { ClientCriterion::CommRegex(t) => string!(t, Comm, true), ClientCriterion::Exe(t) => string!(t, Exe, false), ClientCriterion::ExeRegex(t) => string!(t, Exe, true), + ClientCriterion::Tag(t) => string!(t, Tag, false), + ClientCriterion::TagRegex(t) => string!(t, Tag, true), }; let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion }); get_response!( diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs index 0048a465..507e39e3 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -91,6 +91,10 @@ pub enum ClientCriterion<'a> { Exe(&'a str), /// Matches the `/proc/pid/exe` of the client with a regular expression. ExeRegex(&'a str), + /// Matches the tag of the client verbatim. + Tag(&'a str), + /// Matches the tag of the client with a regular expression. + TagRegex(&'a str), } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index f5f4e3c9..4f7dfe69 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2107,6 +2107,7 @@ impl ConfigProxyHandler { } ClientCriterionStringField::Comm => mgr.comm(needle), ClientCriterionStringField::Exe => mgr.exe(needle), + ClientCriterionStringField::Tag => mgr.tag(needle), } } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 99b0ea8b..be88c270 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -12,7 +12,7 @@ use { clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine, - ClmMatchSandboxInstanceId, + ClmMatchSandboxInstanceId, ClmMatchTag, }, clmm_uid::ClmMatchUid, }, @@ -61,6 +61,7 @@ pub struct RootMatchers { pid: ClmRootMatcherMap, comm: ClmRootMatcherMap, exe: ClmRootMatcherMap, + tag: ClmRootMatcherMap, } impl RootMatchers { @@ -72,6 +73,7 @@ impl RootMatchers { self.pid.clear(); self.comm.clear(); self.exe.clear(); + self.tag.clear(); } } @@ -181,6 +183,7 @@ impl ClMatcherManager { unconditional!(pid); unconditional!(comm); unconditional!(exe); + unconditional!(tag); fixed!(sandboxed); fixed!(is_xwayland); self.constant[true].handle(data); @@ -222,6 +225,10 @@ impl ClMatcherManager { pub fn exe(&self, string: CritLiteralOrRegex) -> Rc { self.root(ClmMatchExe::new(string)) } + + pub fn tag(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchTag::new(string)) + } } impl CritTarget for Rc { diff --git a/src/criteria/clm/clm_matchers/clmm_string.rs b/src/criteria/clm/clm_matchers/clmm_string.rs index 626c5f3d..a5877adc 100644 --- a/src/criteria/clm/clm_matchers/clmm_string.rs +++ b/src/criteria/clm/clm_matchers/clmm_string.rs @@ -15,6 +15,7 @@ pub type ClmMatchString = CritMatchString, T>; pub type ClmMatchSandboxEngine = ClmMatchString>; pub type ClmMatchSandboxAppId = ClmMatchString>; pub type ClmMatchSandboxInstanceId = ClmMatchString>; +pub type ClmMatchTag = ClmMatchString>; pub type ClmMatchComm = ClmMatchString; pub type ClmMatchExe = ClmMatchString; @@ -22,7 +23,7 @@ pub struct AcceptorMetadataAccess(PhantomData); pub struct CommAccess; pub struct ExeAccess; -trait SandboxField: Sized + 'static { +trait AcceptorMetadataField: Sized + 'static { fn field(meta: &AcceptorMetadata) -> &Option; fn nodes( roots: &RootMatchers, @@ -32,10 +33,11 @@ trait SandboxField: Sized + 'static { pub struct SandboxEngineField; pub struct SandboxAppIdField; pub struct SandboxInstanceIdField; +pub struct TagField; impl StringAccess> for AcceptorMetadataAccess where - T: SandboxField, + T: AcceptorMetadataField, { fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { f(T::field(&data.acceptor).as_deref().unwrap_or_default()) @@ -46,7 +48,7 @@ where } } -impl SandboxField for SandboxEngineField { +impl AcceptorMetadataField for SandboxEngineField { fn field(meta: &AcceptorMetadata) -> &Option { &meta.sandbox_engine } @@ -58,7 +60,7 @@ impl SandboxField for SandboxEngineField { } } -impl SandboxField for SandboxAppIdField { +impl AcceptorMetadataField for SandboxAppIdField { fn field(meta: &AcceptorMetadata) -> &Option { &meta.app_id } @@ -70,7 +72,7 @@ impl SandboxField for SandboxAppIdField { } } -impl SandboxField for SandboxInstanceIdField { +impl AcceptorMetadataField for SandboxInstanceIdField { fn field(meta: &AcceptorMetadata) -> &Option { &meta.instance_id } @@ -82,6 +84,18 @@ impl SandboxField for SandboxInstanceIdField { } } +impl AcceptorMetadataField for TagField { + fn field(meta: &AcceptorMetadata) -> &Option { + &meta.tag + } + + fn nodes( + roots: &RootMatchers, + ) -> &ClmRootMatcherMap>> { + &roots.tag + } +} + impl StringAccess> for CommAccess { fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { f(&data.pid_info.comm) diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index e15b175b..65fdf9e8 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -278,6 +278,8 @@ pub struct ClientMatch { pub comm_regex: Option, pub exe: Option, pub exe_regex: Option, + pub tag: Option, + pub tag_regex: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/client_match.rs b/toml-config/src/config/parsers/client_match.rs index 013e7646..24ecbc08 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -54,12 +54,14 @@ impl Parser for ClientMatchParser<'_> { sandbox_instance_id_regex, uid, pid, - is_xwayland, comm, comm_regex, exe, exe_regex, + tag, + tag_regex, ), + (is_xwayland,), ) = ext.extract(( ( opt(str("name")), @@ -78,12 +80,14 @@ impl Parser for ClientMatchParser<'_> { opt(str("sandbox-instance-id-regex")), opt(s32("uid")), opt(s32("pid")), - opt(bol("is-xwayland")), opt(str("comm")), opt(str("comm-regex")), opt(str("exe")), opt(str("exe-regex")), + opt(str("tag")), + opt(str("tag-regex")), ), + (opt(bol("is-xwayland")),), ))?; let mut not = None; if let Some(value) = not_val { @@ -130,6 +134,8 @@ impl Parser for ClientMatchParser<'_> { comm_regex: comm_regex.despan_into(), exe: exe.despan_into(), exe_regex: exe_regex.despan_into(), + tag: tag.despan_into(), + tag_regex: tag_regex.despan_into(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index ef8da965..b67a2cca 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -127,6 +127,8 @@ impl Rule for ClientRule { value_ref!(CommRegex, comm_regex); value_ref!(Exe, exe); value_ref!(ExeRegex, exe_regex); + value_ref!(Tag, tag); + value_ref!(TagRegex, tag_regex); value!(Uid, uid); value!(Pid, pid); bool!(Sandboxed, sandboxed); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 29052508..ef992aba 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -742,6 +742,14 @@ "exe-regex": { "type": "string", "description": "Matches the `/proc/pid/exe` of the client with a regular expression." + }, + "tag": { + "type": "string", + "description": "Matches the tag of the client verbatim." + }, + "tag-regex": { + "type": "string", + "description": "Matches the tag of the client with a regular expression." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 58d2beb1..71bc4ef9 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1242,6 +1242,18 @@ The table has the following fields: The value of this field should be a string. +- `tag` (optional): + + Matches the tag of the client verbatim. + + The value of this field should be a string. + +- `tag-regex` (optional): + + Matches the tag of the client with a regular expression. + + The value of this field should be a string. + ### `ClientMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index de751055..c4336c9b 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3823,6 +3823,14 @@ ClientMatch: kind: string required: false description: Matches the `/proc/pid/exe` of the client with a regular expression. + tag: + kind: string + required: false + description: Matches the tag of the client verbatim. + tag-regex: + kind: string + required: false + description: Matches the tag of the client with a regular expression. ClientMatchExactly: