From ac71cd4c843ff4275c1e22a45cbd9953332b47f9 Mon Sep 17 00:00:00 2001 From: MithicSpirit Date: Sat, 30 May 2026 15:17:39 -0400 Subject: [PATCH] feat: add support for xdg-toplevel-tag-v1 protocol The tag is exposed to window rules via xdg-tag matcher, and it is also provided via IPC. --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- niri-config/src/lib.rs | 3 +++ niri-config/src/window_rule.rs | 2 ++ niri-ipc/src/lib.rs | 2 ++ src/handlers/mod.rs | 5 +++++ src/ipc/client.rs | 7 +++++++ src/ipc/server.rs | 11 +++++++++-- src/niri.rs | 4 ++++ src/utils/mod.rs | 19 +++++++++++++++++++ src/window/mod.rs | 23 +++++++++++++++++++---- 11 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b03ba15c06..c627cb6cdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3498,7 +3498,7 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/Smithay/smithay.git?rev=ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef#ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef" +source = "git+https://github.com/MithicSpirit/smithay.git?rev=3aa06c55bc0f3ec97b441d41add221b2aac86109#3aa06c55bc0f3ec97b441d41add221b2aac86109" dependencies = [ "aliasable", "appendlist", diff --git a/Cargo.toml b/Cargo.toml index fa7164e97e..08ccd15b19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ tracy-client = { version = "0.18.4", default-features = false } [workspace.dependencies.smithay] # version = "0.4.1" -git = "https://github.com/Smithay/smithay.git" -rev = "ff5fa7df392cecfba049ffed55cdaa4e98a8e7ef" +git = "https://github.com/MithicSpirit/smithay.git" +rev = "3aa06c55bc0f3ec97b441d41add221b2aac86109" default-features = false [workspace.dependencies.smithay-drm-extras] diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 77f0796741..98337d0999 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1737,6 +1737,7 @@ mod tests { is_window_cast_target: None, is_urgent: None, at_startup: None, + xdg_tag: None, }, ], excludes: [ @@ -1756,6 +1757,7 @@ mod tests { is_window_cast_target: None, is_urgent: None, at_startup: None, + xdg_tag: None, }, Match { app_id: None, @@ -1771,6 +1773,7 @@ mod tests { is_window_cast_target: None, is_urgent: None, at_startup: None, + xdg_tag: None, }, ], default_column_width: None, diff --git a/niri-config/src/window_rule.rs b/niri-config/src/window_rule.rs index f2bc2ad157..4be5ad3c51 100644 --- a/niri-config/src/window_rule.rs +++ b/niri-config/src/window_rule.rs @@ -137,6 +137,8 @@ pub struct Match { pub is_urgent: Option, #[knuffel(property)] pub at_startup: Option, + #[knuffel(property, str)] + pub xdg_tag: Option, } #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)] diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 218e532cb9..89a82bdc53 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -1368,6 +1368,8 @@ pub struct Window { /// /// The timestamp comes from the monotonic clock. pub focus_timestamp: Option, + /// xdg-toplevel-tag tag and description, if set. + pub xdg_tag: (Option, Option), } /// A moment in time. diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 3532078b6a..7dba0b19de 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -61,6 +61,7 @@ use smithay::wayland::tablet_manager::TabletSeatHandler; use smithay::wayland::xdg_activation::{ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, }; +use smithay::wayland::xdg_toplevel_tag::XdgToplevelTagHandler; use smithay::{ delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf, delegate_drm_lease, delegate_ext_data_control, delegate_fractional_scale, @@ -70,6 +71,7 @@ use smithay::{ delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock, delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation, + delegate_xdg_toplevel_tag, }; pub use crate::handlers::xdg_shell::KdeDecorationsModeState; @@ -861,3 +863,6 @@ impl MutterX11InteropHandler for State {} delegate_mutter_x11_interop!(State); delegate_single_pixel_buffer!(State); + +impl XdgToplevelTagHandler for State {} +delegate_xdg_toplevel_tag!(State); diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 1ec51c4497..704ca661a2 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -691,6 +691,13 @@ fn print_window(window: &Window) { println!(" App ID: (unset)"); } + match &window.xdg_tag { + (Some(tag), Some(description)) => println!(" XDG Tag: {description} [{tag}]"), + (None, Some(description)) => println!(" XDG Tag: {description}"), + (Some(tag), None) => println!(" XDG Tag: [{tag}]"), + (None, None) => println!(" XDG Tag: (unset)"), + } + println!( " Is floating: {}", if window.is_floating { "yes" } else { "no" } diff --git a/src/ipc/server.rs b/src/ipc/server.rs index db71da6dbd..ccf05efe32 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -34,7 +34,7 @@ use crate::backend::IpcOutputMap; use crate::input::pick_window_grab::PickWindowGrab; use crate::layout::workspace::WorkspaceId; use crate::niri::State; -use crate::utils::{version, with_toplevel_role}; +use crate::utils::{version, with_toplevel_role, with_toplevel_role_and_tag}; use crate::window::Mapped; // If an event stream client fails to read events fast enough that we accumulate more than this @@ -517,10 +517,17 @@ fn make_ipc_window( workspace_id: Option, layout: WindowLayout, ) -> niri_ipc::Window { - with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window { + with_toplevel_role_and_tag(mapped.toplevel(), |role, tag| niri_ipc::Window { id: mapped.id().get(), title: role.title.clone(), app_id: role.app_id.clone(), + xdg_tag: match tag { + Some(tag) => ( + tag.tag().map(|x| x.as_ref().to_owned()), + tag.description().map(|x| x.as_ref().to_owned()), + ), + None => (None, None), + }, pid: mapped.credentials().map(|c| c.pid), workspace_id: workspace_id.map(|id| id.get()), is_focused: mapped.is_focused(), diff --git a/src/niri.rs b/src/niri.rs index a4150893e9..9478e14c3f 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -111,6 +111,7 @@ use smithay::wayland::viewporter::ViewporterState; use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState; use smithay::wayland::xdg_activation::XdgActivationState; use smithay::wayland::xdg_foreign::XdgForeignState; +use smithay::wayland::xdg_toplevel_tag::XdgToplevelTagManager; use wayland_server::protocol::wl_output::WlOutput; #[cfg(feature = "dbus")] @@ -312,6 +313,7 @@ pub struct Niri { pub gamma_control_manager_state: GammaControlManagerState, pub activation_state: XdgActivationState, pub mutter_x11_interop_state: MutterX11InteropManagerState, + pub xdg_toplevel_tag_manager: XdgToplevelTagManager, // This will not work as is outside of tests, so it is gated with #[cfg(test)] for now. In // particular, shaders will need to learn about the single pixel buffer. Also, it must be @@ -2367,6 +2369,7 @@ impl Niri { let mutter_x11_interop_state = MutterX11InteropManagerState::new::(&display_handle, move |_| true); + let xdg_toplevel_tag_manager = XdgToplevelTagManager::new::(&display_handle); #[cfg(test)] let single_pixel_buffer_state = SinglePixelBufferState::new::(&display_handle); @@ -2561,6 +2564,7 @@ impl Niri { gamma_control_manager_state, activation_state, mutter_x11_interop_state, + xdg_toplevel_tag_manager, #[cfg(test)] single_pixel_buffer_state, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bbeac5bc6a..a2caed1ddb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -31,6 +31,7 @@ use smithay::wayland::shell::xdg::{ ToplevelCachedState, ToplevelConfigure, ToplevelState, ToplevelSurface, XdgToplevelSurfaceData, XdgToplevelSurfaceRoleAttributes, }; +use smithay::wayland::xdg_toplevel_tag::XdgToplevelTagSurfaceData; use wayland_backend::server::Credentials; use crate::handlers::KdeDecorationsModeState; @@ -356,6 +357,24 @@ pub fn with_toplevel_role( }) } +pub fn with_toplevel_role_and_tag( + toplevel: &ToplevelSurface, + f: impl FnOnce(&mut XdgToplevelSurfaceRoleAttributes, Option<&XdgToplevelTagSurfaceData>) -> T, +) -> T { + with_states(toplevel.wl_surface(), |states| { + let mut role = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + + let tag = states.data_map.get::(); + + f(&mut role, tag) + }) +} + pub fn with_toplevel_role_and_current( toplevel: &ToplevelSurface, f: impl FnOnce(&mut XdgToplevelSurfaceRoleAttributes, Option<&ToplevelState>) -> T, diff --git a/src/window/mod.rs b/src/window/mod.rs index 14527cd242..7cc0baf177 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -13,8 +13,9 @@ use smithay::wayland::compositor::with_states; use smithay::wayland::shell::xdg::{ SurfaceCachedState, ToplevelSurface, XdgToplevelSurfaceRoleAttributes, }; +use smithay::wayland::xdg_toplevel_tag::XdgToplevelTagSurfaceData; -use crate::utils::with_toplevel_role; +use crate::utils::with_toplevel_role_and_tag; pub mod mapped; pub use mapped::Mapped; @@ -185,7 +186,7 @@ impl ResolvedWindowRules { let mut resolved = ResolvedWindowRules::default(); - with_toplevel_role(window.toplevel(), |role| { + with_toplevel_role_and_tag(window.toplevel(), |role, tag| { // Ensure server_pending like in Smithay's with_pending_state(). if role.server_pending.is_none() { role.server_pending = Some(role.current_server_state().clone()); @@ -202,7 +203,7 @@ impl ResolvedWindowRules { } } - window_matches(window, role, m) + window_matches(window, role, tag, m) }; if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) { @@ -383,7 +384,12 @@ impl ResolvedWindowRules { } } -fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool { +fn window_matches( + window: WindowRef, + role: &XdgToplevelSurfaceRoleAttributes, + tag_data: Option<&XdgToplevelTagSurfaceData>, + m: &Match, +) -> bool { // Must be ensured by the caller. let server_pending = role.server_pending.as_ref().unwrap(); @@ -427,6 +433,15 @@ fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: } } + if let Some(tag_re) = &m.xdg_tag { + let Some(tag) = &tag_data.and_then(|x| x.tag()) else { + return false; + }; + if !tag_re.0.is_match(tag.as_ref()) { + return false; + } + } + if let Some(is_active_in_column) = m.is_active_in_column { if window.is_active_in_column() != is_active_in_column { return false;