From 4fd70f03e16fe79b8b2f8d4b7865ab995184a96f Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 17 Jul 2025 11:02:32 +0200 Subject: [PATCH] config: add content-type window criteria --- docs/window-and-client-rules.md | 3 +- jay-config/src/_private.rs | 3 +- jay-config/src/_private/client.rs | 12 ++++- jay-config/src/_private/ipc.rs | 8 ++- jay-config/src/window.rs | 22 ++++++++ src/config/handler.rs | 20 ++++++- src/criteria/tlm.rs | 11 +++- src/criteria/tlm/tlm_matchers.rs | 1 + .../tlm/tlm_matchers/tlmm_content_type.rs | 32 +++++++++++ src/ifs/wl_surface/x_surface.rs | 3 ++ src/ifs/wl_surface/x_surface/xwindow.rs | 1 + .../wl_surface/xdg_surface/xdg_toplevel.rs | 22 +++++--- src/ifs/wp_content_type_v1.rs | 19 +++++++ src/tree/toplevel.rs | 15 ++++-- toml-config/src/config.rs | 3 +- toml-config/src/config/parsers.rs | 1 + .../src/config/parsers/content_type.rs | 53 +++++++++++++++++++ .../src/config/parsers/window_match.rs | 10 ++++ toml-config/src/rules.rs | 3 ++ toml-spec/spec/spec.generated.json | 28 ++++++++++ toml-spec/spec/spec.generated.md | 47 ++++++++++++++++ toml-spec/spec/spec.yaml | 28 ++++++++++ 22 files changed, 327 insertions(+), 18 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_content_type.rs create mode 100644 toml-config/src/config/parsers/content_type.rs diff --git a/docs/window-and-client-rules.md b/docs/window-and-client-rules.md index 48b7be77..88a9590b 100644 --- a/docs/window-and-client-rules.md +++ b/docs/window-and-client-rules.md @@ -249,7 +249,6 @@ The full specification of window criteria can be found in - `urgent` - Matches if the window wants/doesn't want attentions. - `focused` - Matches if the window is/isn't focused. - `fullscreen` - Matches if the window is/isn't fullscreen. -- `just-mapped` - Matches if the window has/hasn't just been mapped. This is - `just-mapped` - Matches if the window has/hasn't just been mapped. This is true for a single frame after the window has been mapped. - `tag`, `tag-regex` - Matches the XDG toplevel tag of the window. @@ -257,3 +256,5 @@ The full specification of window criteria can be found in - `x-instance`, `x-instance-regex` - Matches the X instance of the window. - `x-role`, `x-role-regex` - Matches the X role of the window. - `workspace`, `workspace-regex` - Matches the workspace of the window. +- `content-types` - Matches the content type of a window. Currently there are + three types: photos, videos, and games. diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index d968223e..62715732 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -9,7 +9,7 @@ use { client::ClientMatcher, input::Seat, video::Mode, - window::{WindowMatcher, WindowType}, + window::{ContentType, WindowMatcher, WindowType}, }, bincode::Options, serde::{Deserialize, Serialize}, @@ -119,6 +119,7 @@ pub enum WindowCriterionIpc { Fullscreen, JustMapped, Workspace(Workspace), + ContentTypes(ContentType), } #[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 d7894faa..2a3c0710 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -32,7 +32,10 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, - window::{MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType}, + window::{ + ContentType, MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, + WindowType, + }, xwayland::XScalingMode, }, bincode::Options, @@ -413,6 +416,12 @@ impl ConfigClient { kind } + pub fn content_type(&self, window: Window) -> ContentType { + let res = self.send_with_response(&ClientMessage::GetContentType { window }); + get_response!(res, ContentType(0), GetContentType { kind }); + kind + } + pub fn window_id(&self, window: Window) -> String { let res = self.send_with_response(&ClientMessage::GetWindowId { window }); get_response!(res, String::new(), GetWindowId { id }); @@ -1682,6 +1691,7 @@ impl ConfigClient { WindowCriterion::Workspace(t) => WindowCriterionIpc::Workspace(t), WindowCriterion::WorkspaceName(t) => string!(t, Workspace, false), WindowCriterion::WorkspaceNameRegex(t) => string!(t, Workspace, true), + WindowCriterion::ContentTypes(t) => WindowCriterionIpc::ContentTypes(t), }; let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); get_response!( diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 7fa1830a..2e5d5bf2 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -15,7 +15,7 @@ use { ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode, connector_type::ConnectorType, }, - window::{TileState, Window, WindowMatcher, WindowType}, + window::{ContentType, TileState, Window, WindowMatcher, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -718,6 +718,9 @@ pub enum ClientMessage<'a> { device: InputDevice, enabled: bool, }, + GetContentType { + window: Window, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -944,6 +947,9 @@ pub enum Response { CreateWindowMatcher { matcher: WindowMatcher, }, + GetContentType { + kind: ContentType, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index df949770..610e8580 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -41,6 +41,21 @@ bitflags! { } } +bitflags! { + /// The content type of a window. + #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)] + pub struct ContentType(pub u64) { + /// No content type. + pub const NO_CONTENT_TYPE = 1 << 0, + /// Photo content type. + pub const PHOTO_CONTENT = 1 << 1, + /// Video content type. + pub const VIDEO_CONTENT = 1 << 2, + /// Game content type. + pub const GAME_CONTENT = 1 << 3, + } +} + /// The tile state of a window. #[non_exhaustive] #[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -86,6 +101,11 @@ impl Window { get!(WindowType(0)).window_type(self) } + /// Returns the content type of the window. + pub fn content_type(self) -> ContentType { + get!(ContentType(0)).content_type(self) + } + /// Returns the identifier of the window. /// /// This is the identifier used in the `ext-foreign-toplevel-list-v1` protocol. @@ -292,6 +312,8 @@ pub enum WindowCriterion<'a> { WorkspaceName(&'a str), /// Matches the workspace name of the window with a regular expression. WorkspaceNameRegex(&'a str), + /// Matches if the window has one of the content types. + ContentTypes(ContentType), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 84159f90..0f34717c 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -16,7 +16,10 @@ use { tlm::{TlmLeafMatcher, TlmUpstreamNode}, }, format::config_formats, - ifs::wl_seat::{SeatId, WlSeatGlobal}, + ifs::{ + wl_seat::{SeatId, WlSeatGlobal}, + wp_content_type_v1::ContentTypeExt, + }, io_uring::TaskResultExt, kbvm::{KbvmError, KbvmMap}, output_schedule::map_cursor_hz, @@ -2062,6 +2065,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Workspace(w) => mgr.workspace(CritLiteralOrRegex::Literal( self.get_workspace(*w)?.to_string(), )), + WindowCriterionIpc::ContentTypes(t) => mgr.content_type(*t), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), @@ -2356,6 +2360,17 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_content_type(&self, window: Window) -> Result<(), CphError> { + let kind = self + .get_window(window)? + .tl_data() + .content_type + .get() + .to_config(); + self.respond(Response::GetContentType { kind }); + Ok(()) + } + fn handle_window_exists(&self, window: Window) { self.respond(Response::WindowExists { exists: self.get_window(window).is_ok(), @@ -2964,6 +2979,9 @@ impl ConfigProxyHandler { ClientMessage::SetMiddleButtonEmulationEnabled { device, enabled } => self .handle_set_middle_button_emulation_enabled(device, enabled) .wrn("set_middle_button_emulation_enabled")?, + ClientMessage::GetContentType { window } => self + .handle_get_content_type(window) + .wrn("get_content_type")?, } Ok(()) } diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index cdb01a6f..cdaeac99 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -13,6 +13,7 @@ use { crit_matchers::critm_constant::CritMatchConstant, tlm::tlm_matchers::{ tlmm_client::TlmMatchClient, + tlmm_content_type::TlmMatchContentType, tlmm_floating::TlmMatchFloating, tlmm_fullscreen::TlmMatchFullscreen, tlmm_just_mapped::TlmMatchJustMapped, @@ -34,7 +35,7 @@ use { toplevel_identifier::ToplevelIdentifier, }, }, - jay_config::window::WindowType, + jay_config::window::{ContentType, WindowType}, linearize::static_map, std::{ marker::PhantomData, @@ -58,6 +59,7 @@ bitflags! { TL_CHANGED_CLASS_INST = 1 << 11, TL_CHANGED_ROLE = 1 << 12, TL_CHANGED_WORKSPACE = 1 << 13, + TL_CHANGED_CONTENT_TY = 1 << 14, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -90,6 +92,7 @@ pub struct RootMatchers { instance: TlmRootMatcherMap, role: TlmRootMatcherMap, workspace: TlmRootMatcherMap, + content_ty: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -222,6 +225,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_CLASS_INST, instance); conditional!(TL_CHANGED_ROLE, role); conditional!(TL_CHANGED_WORKSPACE, workspace); + conditional!(TL_CHANGED_CONTENT_TY, content_ty); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -299,6 +303,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_CLASS_INST, instance); conditional!(TL_CHANGED_ROLE, role); conditional!(TL_CHANGED_WORKSPACE, workspace); + conditional!(TL_CHANGED_CONTENT_TY, content_ty); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -372,6 +377,10 @@ impl TlMatcherManager { pub fn workspace(&self, string: CritLiteralOrRegex) -> Rc { self.root(TlmMatchWorkspace::new(string)) } + + pub fn content_type(&self, kind: ContentType) -> Rc { + self.root(TlmMatchContentType::new(kind)) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 6d900774..ccf42900 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -18,6 +18,7 @@ macro_rules! fixed_root_criterion { } pub mod tlmm_client; +pub mod tlmm_content_type; pub mod tlmm_floating; pub mod tlmm_fullscreen; pub mod tlmm_just_mapped; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_content_type.rs b/src/criteria/tlm/tlm_matchers/tlmm_content_type.rs new file mode 100644 index 00000000..a7878b6d --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_content_type.rs @@ -0,0 +1,32 @@ +use { + crate::{ + criteria::{ + crit_graph::CritRootCriterion, + tlm::{RootMatchers, TlmRootMatcherMap}, + }, + ifs::wp_content_type_v1::ContentTypeExt, + tree::ToplevelData, + utils::bitflags::BitflagsExt, + }, + jay_config::window::ContentType, +}; + +pub struct TlmMatchContentType { + kind: ContentType, +} + +impl TlmMatchContentType { + pub fn new(kind: ContentType) -> TlmMatchContentType { + Self { kind } + } +} + +impl CritRootCriterion for TlmMatchContentType { + fn matches(&self, data: &ToplevelData) -> bool { + self.kind.0.contains(data.content_type.get().to_config().0) + } + + fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap> { + Some(&roots.content_ty) + } +} diff --git a/src/ifs/wl_surface/x_surface.rs b/src/ifs/wl_surface/x_surface.rs index 028b89dd..3fe6ffbc 100644 --- a/src/ifs/wl_surface/x_surface.rs +++ b/src/ifs/wl_surface/x_surface.rs @@ -26,6 +26,9 @@ impl SurfaceExt for XSurface { fn after_apply_commit(self: Rc) { if let Some(xwindow) = self.xwindow.get() { xwindow.map_status_changed(); + xwindow + .toplevel_data + .set_content_type(self.surface.content_type.get()); } } diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 66d1a31c..07b3ae72 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -217,6 +217,7 @@ impl Xwindow { weak, ); tld.pos.set(surface.extents.get()); + tld.content_type.set(surface.content_type.get()); Self { id, data: data.clone(), diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 67c5fe08..07af5234 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -146,6 +146,17 @@ impl XdgToplevel { let data = Rc::new(XdgToplevelToplevelData { tag: Default::default(), }); + let toplevel_data = ToplevelData::new( + state, + String::new(), + Some(surface.surface.client.clone()), + ToplevelType::XdgToplevel(data.clone()), + node_id, + slf, + ); + toplevel_data + .content_type + .set(surface.surface.content_type.get()); Self { id, state: state.clone(), @@ -161,14 +172,7 @@ impl XdgToplevel { max_width: Cell::new(None), max_height: Cell::new(None), tracker: Default::default(), - toplevel_data: ToplevelData::new( - state, - String::new(), - Some(surface.surface.client.clone()), - ToplevelType::XdgToplevel(data.clone()), - node_id, - slf, - ), + toplevel_data, drag: Default::default(), is_mapped: Cell::new(false), dialog: Default::default(), @@ -518,6 +522,8 @@ impl XdgToplevel { self.state.tree_changed(); self.toplevel_data.broadcast(self.clone()); } + self.toplevel_data + .set_content_type(self.xdg.surface.content_type.get()); } } diff --git a/src/ifs/wp_content_type_v1.rs b/src/ifs/wp_content_type_v1.rs index 0f60e715..16f3727d 100644 --- a/src/ifs/wp_content_type_v1.rs +++ b/src/ifs/wp_content_type_v1.rs @@ -6,6 +6,10 @@ use { object::{Object, Version}, wire::{WpContentTypeV1Id, wp_content_type_v1::*}, }, + jay_config::window::{ + ContentType as ConfigContentType, GAME_CONTENT, NO_CONTENT_TYPE, PHOTO_CONTENT, + VIDEO_CONTENT, + }, std::rc::Rc, thiserror::Error, }; @@ -22,6 +26,21 @@ pub enum ContentType { Game, } +pub trait ContentTypeExt { + fn to_config(&self) -> ConfigContentType; +} + +impl ContentTypeExt for Option { + fn to_config(&self) -> ConfigContentType { + match self { + None => NO_CONTENT_TYPE, + Some(ContentType::Photo) => PHOTO_CONTENT, + Some(ContentType::Video) => VIDEO_CONTENT, + Some(ContentType::Game) => GAME_CONTENT, + } + } +} + pub struct WpContentTypeV1 { pub id: WpContentTypeV1Id, pub client: Rc, diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 58801503..f45f4bbf 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -4,9 +4,9 @@ use { criteria::{ CritDestroyListener, CritMatcherId, tlm::{ - TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, - TL_CHANGED_FULLSCREEN, TL_CHANGED_NEW, TL_CHANGED_TITLE, TL_CHANGED_URGENT, - TL_CHANGED_VISIBLE, TL_CHANGED_WORKSPACE, TlMatcherChange, + TL_CHANGED_APP_ID, TL_CHANGED_CONTENT_TY, TL_CHANGED_DESTROYED, + TL_CHANGED_FLOATING, TL_CHANGED_FULLSCREEN, TL_CHANGED_NEW, TL_CHANGED_TITLE, + TL_CHANGED_URGENT, TL_CHANGED_VISIBLE, TL_CHANGED_WORKSPACE, TlMatcherChange, }, }, ifs::{ @@ -20,6 +20,7 @@ use { WlSurface, x_surface::xwindow::XwindowData, xdg_surface::xdg_toplevel::XdgToplevelToplevelData, }, + wp_content_type_v1::ContentType, zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1, zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1, }, @@ -371,6 +372,7 @@ pub struct ToplevelData { pub changed_properties: Cell, pub just_mapped_scheduled: Cell, pub seat_foci: CopyHashMap, + pub content_type: Cell>, } impl ToplevelData { @@ -422,6 +424,7 @@ impl ToplevelData { changed_properties: Default::default(), just_mapped_scheduled: Cell::new(false), seat_foci: Default::default(), + content_type: Default::default(), } } @@ -857,6 +860,12 @@ impl ToplevelData { pub fn just_mapped(&self) -> bool { self.mapped_during_iteration.get() == self.state.eng.iteration() } + + pub fn set_content_type(&self, content_type: Option) { + if self.content_type.replace(content_type) != content_type { + self.property_changed(TL_CHANGED_CONTENT_TY); + } + } } impl Drop for ToplevelData { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 7896ddd2..7c2ccc32 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -28,7 +28,7 @@ use { status::MessageFormat, theme::Color, video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, - window::{TileState, WindowType}, + window::{ContentType, TileState, WindowType}, xwayland::XScalingMode, }, std::{ @@ -277,6 +277,7 @@ pub struct WindowMatch { pub x_role_regex: Option, pub workspace: Option, pub workspace_regex: Option, + pub content_types: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index ed064b44..70fbf39e 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -15,6 +15,7 @@ pub mod color_management; pub mod config; mod connector; mod connector_match; +mod content_type; mod drm_device; mod drm_device_match; mod env; diff --git a/toml-config/src/config/parsers/content_type.rs b/toml-config/src/config/parsers/content_type.rs new file mode 100644 index 00000000..c7038bbf --- /dev/null +++ b/toml-config/src/config/parsers/content_type.rs @@ -0,0 +1,53 @@ +use { + crate::{ + config::parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + toml::{ + toml_span::{Span, Spanned, SpannedExt}, + toml_value::Value, + }, + }, + jay_config::window::{ + ContentType, GAME_CONTENT, NO_CONTENT_TYPE, PHOTO_CONTENT, VIDEO_CONTENT, + }, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum ContentTypeParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown content type `{}`", .0)] + UnknownContentType(String), +} + +pub struct ContentTypeParser; + +impl Parser for ContentTypeParser { + type Value = ContentType; + type Error = ContentTypeParserError; + const EXPECTED: &'static [DataType] = &[DataType::Array, DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let ty = match string { + "none" => NO_CONTENT_TYPE, + "any" => !NO_CONTENT_TYPE, + "photo" => PHOTO_CONTENT, + "video" => VIDEO_CONTENT, + "game" => GAME_CONTENT, + _ => { + return Err( + ContentTypeParserError::UnknownContentType(string.to_owned()).spanned(span), + ); + } + }; + Ok(ty) + } + + fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { + let mut ty = ContentType(0); + for el in array { + ty |= el.parse(&mut ContentTypeParser)?; + } + Ok(ty) + } +} diff --git a/toml-config/src/config/parsers/window_match.rs b/toml-config/src/config/parsers/window_match.rs index b8479762..a2c11305 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -7,6 +7,7 @@ use { parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ client_match::{ClientMatchParser, ClientMatchParserError}, + content_type::{ContentTypeParser, ContentTypeParserError}, window_type::{WindowTypeParser, WindowTypeParserError}, }, }, @@ -29,6 +30,8 @@ pub enum WindowMatchParserError { WindowTypes(#[from] WindowTypeParserError), #[error(transparent)] ClientMatchParserError(#[from] ClientMatchParserError), + #[error(transparent)] + ContentTypes(#[from] ContentTypeParserError), } pub struct WindowMatchParser<'a>(pub &'a Context<'a>); @@ -77,6 +80,7 @@ impl Parser for WindowMatchParser<'_> { x_role_regex, workspace, workspace_regex, + content_types_val, ), ) = ext.extract(( ( @@ -111,6 +115,7 @@ impl Parser for WindowMatchParser<'_> { opt(str("x-role-regex")), opt(str("workspace")), opt(str("workspace-regex")), + opt(val("content-types")), ), ))?; let mut not = None; @@ -144,6 +149,10 @@ impl Parser for WindowMatchParser<'_> { if let Some(value) = client_val { client = Some(value.parse_map(&mut ClientMatchParser(self.0))?); } + let mut content_types = None; + if let Some(value) = content_types_val { + content_types = Some(value.parse_map(&mut ContentTypeParser)?); + } Ok(WindowMatch { generic: GenericMatch { name: name.despan_into(), @@ -174,6 +183,7 @@ impl Parser for WindowMatchParser<'_> { workspace_regex: workspace_regex.despan_into(), types, client, + content_types, }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index ffef34c3..48029314 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -281,6 +281,9 @@ impl Rule for WindowRule { }; all.push(matcher); } + if let Some(value) = &match_.content_types { + all.push(m(WindowCriterion::ContentTypes(*value))); + } Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 00311f57..890e0971 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -924,6 +924,30 @@ } ] }, + "ContentTypeMask": { + "description": "A mask of content types.\n", + "anyOf": [ + { + "type": "string", + "description": "A named mask.", + "enum": [ + "none", + "any", + "photo", + "video", + "game" + ] + }, + { + "type": "array", + "description": "An array of masks that are OR'd.", + "items": { + "description": "", + "$ref": "#/$defs/ContentTypeMask" + } + } + ] + }, "DrmDevice": { "description": "Describes configuration to apply to a DRM device (graphics card).\n\n- Example: To disable direct scanout on a device:\n\n ```toml\n [[drm-devices]]\n match = { pci-vendor = 0x1002, pci-model = 0x73ff }\n direct-scanout = false\n ```\n", "type": "object", @@ -1888,6 +1912,10 @@ "workspace-regex": { "type": "string", "description": "Matches the workspace of the window with a regular expression." + }, + "content-types": { + "description": "Matches windows whose content type is contained in the mask.", + "$ref": "#/$defs/ContentTypeMask" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 132300c7..505d8958 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1855,6 +1855,47 @@ The table has the following fields: The value of this field should be a string. + +### `ContentTypeMask` + +A mask of content types. + +Values of this type should have one of the following forms: + +#### A string + +A named mask. + +The string should have one of the following values: + +- `none`: + + The mask matching windows without a content type. + +- `any`: + + The mask containing every possible type except `none`. + +- `photo`: + + The mask matching photo content. + +- `video`: + + The mask matching video content. + +- `game`: + + The mask matching game content. + + +#### An array + +An array of masks that are OR'd. + +Each element of this array should be a [ContentTypeMask](#types-ContentTypeMask). + + ### `DrmDevice` @@ -4216,6 +4257,12 @@ The table has the following fields: The value of this field should be a string. +- `content-types` (optional): + + Matches windows whose content type is contained in the mask. + + The value of this field should be a [ContentTypeMask](#types-ContentTypeMask). + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 796d4885..9efcc9f4 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3615,6 +3615,10 @@ WindowMatch: kind: string required: false description: Matches the workspace of the window with a regular expression. + content-types: + ref: ContentTypeMask + required: false + description: Matches windows whose content type is contained in the mask. WindowMatchExactly: @@ -3668,3 +3672,27 @@ TileState: description: The window is tiled. - value: floating description: The window is floating. + + +ContentTypeMask: + description: | + A mask of content types. + kind: variable + variants: + - kind: string + description: A named mask. + values: + - value: none + description: The mask matching windows without a content type. + - value: any + description: The mask containing every possible type except `none`. + - value: photo + description: The mask matching photo content. + - value: video + description: The mask matching video content. + - value: game + description: The mask matching game content. + - kind: array + description: An array of masks that are OR'd. + items: + ref: ContentTypeMask