diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 2845a2bc..bd9c227a 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -113,4 +113,6 @@ pub enum WindowCriterionIpc { } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] -pub enum WindowCriterionStringField {} +pub enum WindowCriterionStringField { + Title, +} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 77cb47d2..2bfc0633 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -4,7 +4,8 @@ use { crate::{ _private::{ ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen, - GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, WireMode, bincode_ops, + GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, + WindowCriterionStringField, WireMode, bincode_ops, ipc::{ ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, }, @@ -1609,7 +1610,6 @@ impl ConfigClient { criterion: WindowCriterion, child: bool, ) -> (WindowMatcher, bool) { - #[expect(unused_macros)] macro_rules! string { ($t:expr, $field:ident, $regex:expr) => { WindowCriterionIpc::String { @@ -1653,6 +1653,8 @@ impl ConfigClient { } WindowCriterionIpc::Client(matcher) } + WindowCriterion::Title(t) => string!(t, Title, false), + WindowCriterion::TitleRegex(t) => string!(t, Title, 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 e8aeec2a..18c8b35c 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -236,6 +236,10 @@ pub enum WindowCriterion<'a> { Exactly(usize, &'a [WindowCriterion<'a>]), /// Matches if the window's client matches the client criterion. Client(&'a ClientCriterion<'a>), + /// Matches the title of the window verbatim. + Title(&'a str), + /// Matches the title of the window with a regular expression. + TitleRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index abc51d60..dae15dc4 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -44,7 +44,7 @@ use { jay_config::{ _private::{ ClientCriterionIpc, ClientCriterionStringField, GenericCriterionIpc, PollableId, - WindowCriterionIpc, WireMode, bincode_ops, + WindowCriterionIpc, WindowCriterionStringField, WireMode, bincode_ops, ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, @@ -1980,7 +1980,6 @@ impl ConfigProxyHandler { field, regex, } => { - #[expect(unused_variables)] let needle = match *regex { true => { let regex = Regex::new(string).map_err(CphError::InvalidRegex)?; @@ -1988,7 +1987,9 @@ impl ConfigProxyHandler { } false => CritLiteralOrRegex::Literal(string.to_string()), }; - match *field {} + match *field { + WindowCriterionStringField::Title => mgr.title(needle), + } } WindowCriterionIpc::Types(t) => mgr.kind(*t), WindowCriterionIpc::Client(c) => { diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 96581ece..c751d64c 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -3,13 +3,15 @@ pub mod tlm_matchers; use { crate::{ criteria::{ - CritDestroyListener, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, - FixedRootMatcher, RootMatcherMap, + CritDestroyListener, CritLiteralOrRegex, 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_client::TlmMatchClient, tlmm_kind::TlmMatchKind}, + tlm::tlm_matchers::{ + tlmm_client::TlmMatchClient, tlmm_kind::TlmMatchKind, tlmm_string::TlmMatchTitle, + }, }, state::State, tree::{NodeId, ToplevelData, ToplevelNode}, @@ -26,6 +28,7 @@ bitflags! { TlMatcherChange: u32; TL_CHANGED_DESTROYED = 1 << 0, TL_CHANGED_NEW = 1 << 1, + TL_CHANGED_TITLE = 1 << 2, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -44,6 +47,7 @@ type TlmRootMatcherMap = RootMatcherMap; pub struct RootMatchers { kinds: TlmRootMatcherMap, clients: CopyHashMap>, + title: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -123,7 +127,6 @@ impl TlMatcherManager { } change |= TlMatcherChange::all(); } - #[expect(unused_macros)] macro_rules! conditional { ($change:expr, $field:ident) => { if change.contains($change) && self.matchers.$field.is_not_empty() { @@ -139,6 +142,7 @@ impl TlMatcherManager { } }; } + conditional!(TL_CHANGED_TITLE, title); false } @@ -188,7 +192,6 @@ impl TlMatcherManager { unconditional!(clients); self.constant[true].handle(data); } - #[expect(unused_macros)] macro_rules! conditional { ($change:expr, $field:ident) => { if changed.contains($change) { @@ -206,6 +209,11 @@ impl TlMatcherManager { } }; } + conditional!(TL_CHANGED_TITLE, title); + } + + pub fn title(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchTitle::new(string)) } pub fn kind(&self, kind: WindowType) -> Rc { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 4348b709..d40d6dce 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -20,3 +20,4 @@ macro_rules! fixed_root_criterion { pub mod tlmm_client; pub mod tlmm_kind; +pub mod tlmm_string; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs new file mode 100644 index 00000000..3a56edce --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -0,0 +1,23 @@ +use crate::{ + criteria::{ + crit_matchers::critm_string::{CritMatchString, StringAccess}, + tlm::{RootMatchers, TlmRootMatcherMap}, + }, + tree::ToplevelData, +}; + +pub type TlmMatchString = CritMatchString; + +pub type TlmMatchTitle = TlmMatchString; + +pub struct TitleAccess; + +impl StringAccess for TitleAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + f(&data.title.borrow()) + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.title + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 316a3aba..2f53adc7 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -3,7 +3,7 @@ use { client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, - tlm::{TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TlMatcherChange}, + tlm::{TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TL_CHANGED_TITLE, TlMatcherChange}, }, ifs::{ ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, @@ -92,6 +92,7 @@ impl ToplevelNode for T { .clone_from(&title); data.placeholder.tl_title_changed(); } + data.property_changed(TL_CHANGED_TITLE); } fn tl_set_parent(&self, parent: Rc) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 40bd2e4d..e7eb3342 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -255,6 +255,8 @@ pub struct WindowMatch { pub generic: GenericMatch, pub types: Option, pub client: Option, + pub title: Option, + pub title_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 4836a48b..f4594bd6 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -44,16 +44,27 @@ 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, 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 (( + name, + not_val, + all_val, + any_val, + exactly_val, + types_val, + client_val, + title, + title_regex, + ),) = ext.extract((( + opt(str("name")), + opt(val("not")), + opt(arr("all")), + opt(arr("any")), + opt(val("exactly")), + opt(val("types")), + opt(val("client")), + opt(str("title")), + opt(str("title-regex")), + ),))?; let mut not = None; if let Some(value) = not_val { not = Some(Box::new(value.parse(&mut WindowMatchParser(self.0))?)); @@ -93,6 +104,8 @@ impl Parser for WindowMatchParser<'_> { any, exactly, }, + title: title.despan_into(), + title_regex: title_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index d7cf586b..e3abb988 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -224,7 +224,6 @@ impl Rule for WindowRule { match_: &Self::Match, ) -> Option<()> { let m = |c: WindowCriterion<'_>| MatcherTemp(c.to_matcher()); - #[expect(unused_macros)] macro_rules! value { ($ty:ident, $field:ident) => { if let Some(value) = &match_.$field { @@ -256,6 +255,8 @@ impl Rule for WindowRule { matcher.0, )))); } + value!(Title, title); + value!(TitleRegex, title_regex); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 79ab0227..78a37b49 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1779,6 +1779,14 @@ "client": { "description": "Matches if the window's client matches the client criterion.", "$ref": "#/$defs/ClientMatch" + }, + "title": { + "type": "string", + "description": "Matches the title of the window verbatim." + }, + "title-regex": { + "type": "string", + "description": "Matches the title 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 0b96cbf0..d9a1023f 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4010,6 +4010,18 @@ The table has the following fields: The value of this field should be a [ClientMatch](#types-ClientMatch). +- `title` (optional): + + Matches the title of the window verbatim. + + The value of this field should be a string. + +- `title-regex` (optional): + + Matches the title 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 2f1f2ea3..1282eb43 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3467,6 +3467,14 @@ WindowMatch: ref: ClientMatch required: false description: Matches if the window's client matches the client criterion. + title: + kind: string + required: false + description: Matches the title of the window verbatim. + title-regex: + kind: string + required: false + description: Matches the title of the window with a regular expression. WindowMatchExactly: