From 8b9784bb153bbe0eeed9e1e854777f78fcd400e7 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 4 May 2025 19:02:35 +0200 Subject: [PATCH 01/35] xwayland: kill xwayland when wayland connection fails --- src/client.rs | 8 ++++++++ src/compositor.rs | 1 + src/state.rs | 2 ++ src/utils.rs | 1 + src/utils/pidfd_send_signal.rs | 23 +++++++++++++++++++++++ src/xwayland.rs | 8 +++++++- 6 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/utils/pidfd_send_signal.rs diff --git a/src/client.rs b/src/client.rs index 9fd49a48f..3781cd49 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,6 +19,7 @@ use { numcell::NumCell, pending_serial::PendingSerial, pid_info::{PidInfo, get_pid_info, get_socket_creds}, + pidfd_send_signal::pidfd_send_signal, }, wire::WlRegistryId, }, @@ -251,6 +252,13 @@ impl Drop for ClientHolder { self.data.surfaces_by_xwayland_serial.clear(); self.data.remove_activation_tokens(); self.data.commit_timelines.clear(); + if self.data.is_xwayland { + if let Some(pidfd) = self.data.state.xwayland.pidfd.get() { + if let Err(e) = pidfd_send_signal(&pidfd, c::SIGKILL) { + log::error!("Could not kill Xwayland: {}", ErrorFmt(e)); + } + } + } } } diff --git a/src/compositor.rs b/src/compositor.rs index 02d09347..7574f20f 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -218,6 +218,7 @@ fn start_compositor2( run_args, xwayland: XWaylandState { enabled: Cell::new(true), + pidfd: Default::default(), handler: Default::default(), queue: Default::default(), ipc_device_ids: Default::default(), diff --git a/src/state.rs b/src/state.rs index bc972c53..ff0a278d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -121,6 +121,7 @@ use { time::Duration, }, thiserror::Error, + uapi::OwnedFd, }; pub struct State { @@ -261,6 +262,7 @@ pub struct ScreenlockState { pub struct XWaylandState { pub enabled: Cell, + pub pidfd: CloneCell>>, pub handler: RefCell>>, pub queue: Rc>, pub ipc_device_ids: XIpcDeviceIds, diff --git a/src/utils.rs b/src/utils.rs index ba49cba1..bae62b58 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -43,6 +43,7 @@ pub mod oserror; pub mod page_size; pub mod pending_serial; pub mod pid_info; +pub mod pidfd_send_signal; pub mod process_name; pub mod ptr_ext; pub mod queue; diff --git a/src/utils/pidfd_send_signal.rs b/src/utils/pidfd_send_signal.rs new file mode 100644 index 00000000..74fdeec7 --- /dev/null +++ b/src/utils/pidfd_send_signal.rs @@ -0,0 +1,23 @@ +use { + crate::utils::oserror::OsError, + c::{c_int, syscall}, + std::{ptr, rc::Rc}, + uapi::{ + OwnedFd, + c::{self, SYS_pidfd_send_signal, siginfo_t}, + map_err, + }, +}; + +pub fn pidfd_send_signal(pidfd: &Rc, signal: c_int) -> Result<(), OsError> { + let res = unsafe { + syscall( + SYS_pidfd_send_signal, + pidfd.raw(), + signal, + ptr::null_mut::(), + 0, + ) + }; + map_err!(res).map(drop).map_err(|e| e.into()) +} diff --git a/src/xwayland.rs b/src/xwayland.rs index 949b5f08..ab078f50 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -14,7 +14,9 @@ use { io_uring::IoUringError, state::State, user_session::import_environment, - utils::{buf::Buf, errorfmt::ErrorFmt, line_logger::log_lines, oserror::OsError}, + utils::{ + buf::Buf, errorfmt::ErrorFmt, line_logger::log_lines, on_drop::OnDrop, oserror::OsError, + }, wire::WlSurfaceId, xcon::XconError, xwayland::{ @@ -185,6 +187,10 @@ async fn run( state.update_xwayland_wire_scale(); state.ring.readable(&Rc::new(dfdread)).await?; state.xwayland.queue.clear(); + state.xwayland.pidfd.set(Some(pidfd.clone())); + let _remove_pidfd = OnDrop(|| { + state.xwayland.pidfd.take(); + }); { let shared = Rc::new(XwmShared::default()); let wm = match Wm::get(state, client, wm1, &shared).await { From 0e1868d35570bf32f3cc2e2eb4bee5ab58a5cf65 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Mon, 28 Apr 2025 19:14:12 +0200 Subject: [PATCH 02/35] container: run tl_destroy when replacing container --- src/tree/container.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tree/container.rs b/src/tree/container.rs index 237020d2..7ede6614 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -998,6 +998,9 @@ impl ContainerNode { if let Some(parent) = self.toplevel_data.parent.get() { if !self.toplevel_data.is_fullscreen.get() && parent.cnode_accepts_child(&*child) { parent.cnode_replace_child(self.deref(), child.clone()); + self.toplevel_data.parent.take(); + self.child_nodes.borrow_mut().clear(); + self.tl_destroy(); } } return; From c5818dcd32b40d68cc5104b3653f3244e7efde6e Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 21:25:03 +0200 Subject: [PATCH 03/35] placeholder: run tl_destroy when replacing placeholder --- src/tree/toplevel.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index c8064465..45aed49e 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -552,7 +552,7 @@ impl ToplevelData { state.map_tiled(node); return; } - let parent = fd.placeholder.tl_data().parent.get().unwrap(); + let parent = fd.placeholder.tl_data().parent.take().unwrap(); parent.cnode_replace_child(fd.placeholder.deref(), node.clone()); if node.node_visible() { let kb_foci = collect_kb_foci(fd.placeholder.clone()); @@ -560,9 +560,7 @@ impl ToplevelData { node.clone().node_do_focus(&seat, Direction::Unspecified); } } - fd.placeholder - .node_seat_state() - .destroy_node(fd.placeholder.deref()); + fd.placeholder.tl_destroy(); } pub fn set_visible(&self, node: &dyn Node, visible: bool) { From bc6a9ad94d2f8c8a1e148b1750b85e1707c83f68 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 21:41:21 +0200 Subject: [PATCH 04/35] toml-config: add set/unset variants of toggle actions --- toml-config/src/config.rs | 4 +++ toml-config/src/config/parsers/action.rs | 8 ++++++ toml-config/src/lib.rs | 4 +++ toml-spec/spec/spec.generated.json | 8 ++++++ toml-spec/spec/spec.generated.md | 32 ++++++++++++++++++++++++ toml-spec/spec/spec.yaml | 16 ++++++++++++ 6 files changed, 72 insertions(+) diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index eedd0076..7b4a91c5 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -53,9 +53,13 @@ pub enum SimpleCommand { ReloadConfigToml, Split(Axis), ToggleFloating, + SetFloating(bool), ToggleFullscreen, + SetFullscreen(bool), ToggleMono, + SetMono(bool), ToggleSplit, + SetSplit(Axis), Forward(bool), EnableWindowManagement(bool), SetFloatAboveFullscreen(bool), diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 1bc66f55..4457f42b 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -104,12 +104,20 @@ impl ActionParser<'_> { "split-horizontal" => Split(Horizontal), "split-vertical" => Split(Vertical), "toggle-split" => ToggleSplit, + "tile-horizontal" => SetSplit(Horizontal), + "tile-vertical" => SetSplit(Vertical), "toggle-mono" => ToggleMono, + "show-single" => SetMono(true), + "show-all" => SetMono(false), "toggle-fullscreen" => ToggleFullscreen, + "enter-fullscreen" => SetFullscreen(true), + "exit-fullscreen" => SetFullscreen(false), "focus-parent" => FocusParent, "close" => Close, "disable-pointer-constraint" => DisablePointerConstraint, "toggle-floating" => ToggleFloating, + "float" => SetFloating(true), + "tile" => SetFloating(false), "quit" => Quit, "reload-config-toml" => ReloadConfigToml, "reload-config-so" => ReloadConfigSo, diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index aa136e4e..f19cc546 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -86,14 +86,18 @@ impl Action { SimpleCommand::Move(dir) => B::new(move || s.move_(dir)), SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)), SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()), + SimpleCommand::SetSplit(b) => B::new(move || s.set_split(b)), SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()), + SimpleCommand::SetMono(b) => B::new(move || s.set_mono(b)), SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()), + SimpleCommand::SetFullscreen(b) => B::new(move || s.set_fullscreen(b)), SimpleCommand::FocusParent => B::new(move || s.focus_parent()), SimpleCommand::Close => B::new(move || s.close()), SimpleCommand::DisablePointerConstraint => { B::new(move || s.disable_pointer_constraint()) } SimpleCommand::ToggleFloating => B::new(move || s.toggle_floating()), + SimpleCommand::SetFloating(b) => B::new(move || s.set_floating(b)), SimpleCommand::Quit => B::new(quit), SimpleCommand::ReloadConfigToml => { let persistent = state.persistent.clone(); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 23d55c17..1f05aa76 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1357,12 +1357,20 @@ "split-horizontal", "split-vertical", "toggle-split", + "tile-horizontal", + "tile-vertical", "toggle-mono", + "show-single", + "show-all", "toggle-fullscreen", + "enter-fullscreen", + "exit-fullscreen", "focus-parent", "close", "disable-pointer-constraint", "toggle-floating", + "float", + "tile", "quit", "reload-config-toml", "reload-config-to", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index c12423dd..aeca6373 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -2991,14 +2991,38 @@ The string should have one of the following values: Toggle the split of the currently focused container between vertical and horizontal. +- `tile-horizontal`: + + Sets the split of the currently focused container to horizontal. + +- `tile-vertical`: + + Sets the split of the currently focused container to vertical. + - `toggle-mono`: Toggle the currently focused container between showing a single and all children. +- `show-single`: + + Makes the currently focused container show a single child. + +- `show-all`: + + Makes the currently focused container show all children. + - `toggle-fullscreen`: Toggle the currently focused window between fullscreen and windowed. +- `enter-fullscreen`: + + Makes the currently focused window fullscreen. + +- `exit-fullscreen`: + + Makes the currently focused window windowed. + - `focus-parent`: Focus the parent of the currently focused window. @@ -3018,6 +3042,14 @@ The string should have one of the following values: Toggle the currently focused window between floating and tiled. +- `float`: + + Makes the currently focused window floating. + +- `tile`: + + Makes the currently focused window tiled. + - `quit`: Terminate the compositor. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 86b3c7e5..50761da3 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -726,11 +726,23 @@ SimpleActionName: description: | Toggle the split of the currently focused container between vertical and horizontal. + - value: tile-horizontal + description: Sets the split of the currently focused container to horizontal. + - value: tile-vertical + description: Sets the split of the currently focused container to vertical. - value: toggle-mono description: | Toggle the currently focused container between showing a single and all children. + - value: show-single + description: Makes the currently focused container show a single child. + - value: show-all + description: Makes the currently focused container show all children. - value: toggle-fullscreen description: Toggle the currently focused window between fullscreen and windowed. + - value: enter-fullscreen + description: Makes the currently focused window fullscreen. + - value: exit-fullscreen + description: Makes the currently focused window windowed. - value: focus-parent description: Focus the parent of the currently focused window. - value: close @@ -743,6 +755,10 @@ SimpleActionName: The constraint will be re-enabled when the pointer re-enters the window. - value: toggle-floating description: Toggle the currently focused window between floating and tiled. + - value: float + description: Makes the currently focused window floating. + - value: tile + description: Makes the currently focused window tiled. - value: quit description: Terminate the compositor. - value: reload-config-toml From 52994c085a0e1392332fe393ae991cd53f777332 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 29 Apr 2025 12:52:33 +0200 Subject: [PATCH 05/35] config: add `seat` to seat-based window management messages --- jay-config/src/_private/client.rs | 64 ++++++++--------- jay-config/src/_private/ipc.rs | 32 ++++----- jay-config/src/input.rs | 32 ++++----- src/config/handler.rs | 112 ++++++++++++++++-------------- src/it/test_config.rs | 10 +-- 5 files changed, 127 insertions(+), 123 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2962e56e..fd105478 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -333,12 +333,12 @@ impl Client { self.send(&ClientMessage::GrabKb { kb, grab }); } - pub fn focus(&self, seat: Seat, direction: Direction) { - self.send(&ClientMessage::Focus { seat, direction }); + pub fn seat_focus(&self, seat: Seat, direction: Direction) { + self.send(&ClientMessage::SeatFocus { seat, direction }); } - pub fn move_(&self, seat: Seat, direction: Direction) { - self.send(&ClientMessage::Move { seat, direction }); + pub fn seat_move(&self, seat: Seat, direction: Direction) { + self.send(&ClientMessage::SeatMove { seat, direction }); } pub fn unbind>(&self, seat: Seat, mod_sym: T) { @@ -367,8 +367,8 @@ impl Client { seats } - pub fn mono(&self, seat: Seat) -> bool { - let res = self.send_with_response(&ClientMessage::GetMono { seat }); + pub fn seat_mono(&self, seat: Seat) -> bool { + let res = self.send_with_response(&ClientMessage::GetSeatMono { seat }); get_response!(res, false, GetMono { mono }); mono } @@ -450,12 +450,12 @@ impl Client { self.send(&ClientMessage::ShowWorkspace { seat, workspace }); } - pub fn set_workspace(&self, seat: Seat, workspace: Workspace) { - self.send(&ClientMessage::SetWorkspace { seat, workspace }); + pub fn set_seat_workspace(&self, seat: Seat, workspace: Workspace) { + self.send(&ClientMessage::SetSeatWorkspace { seat, workspace }); } - pub fn split(&self, seat: Seat) -> Axis { - let res = self.send_with_response(&ClientMessage::GetSplit { seat }); + pub fn seat_split(&self, seat: Seat) -> Axis { + let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); axis } @@ -471,12 +471,12 @@ impl Client { }); } - pub fn set_fullscreen(&self, seat: Seat, fullscreen: bool) { - self.send(&ClientMessage::SetFullscreen { seat, fullscreen }); + pub fn set_seat_fullscreen(&self, seat: Seat, fullscreen: bool) { + self.send(&ClientMessage::SetSeatFullscreen { seat, fullscreen }); } - pub fn get_fullscreen(&self, seat: Seat) -> bool { - let res = self.send_with_response(&ClientMessage::GetFullscreen { seat }); + pub fn get_seat_fullscreen(&self, seat: Seat) -> bool { + let res = self.send_with_response(&ClientMessage::GetSeatFullscreen { seat }); get_response!(res, false, GetFullscreen { fullscreen }); fullscreen } @@ -495,18 +495,18 @@ impl Client { font } - pub fn get_floating(&self, seat: Seat) -> bool { - let res = self.send_with_response(&ClientMessage::GetFloating { seat }); + pub fn get_seat_floating(&self, seat: Seat) -> bool { + let res = self.send_with_response(&ClientMessage::GetSeatFloating { seat }); get_response!(res, false, GetFloating { floating }); floating } - pub fn set_floating(&self, seat: Seat, floating: bool) { - self.send(&ClientMessage::SetFloating { seat, floating }); + pub fn set_seat_floating(&self, seat: Seat, floating: bool) { + self.send(&ClientMessage::SetSeatFloating { seat, floating }); } - pub fn toggle_floating(&self, seat: Seat) { - self.set_floating(seat, !self.get_floating(seat)); + pub fn toggle_seat_floating(&self, seat: Seat) { + self.set_seat_floating(seat, !self.get_seat_floating(seat)); } pub fn reset_colors(&self) { @@ -548,8 +548,8 @@ impl Client { self.send(&ClientMessage::SetSize { sized, size }) } - pub fn set_mono(&self, seat: Seat, mono: bool) { - self.send(&ClientMessage::SetMono { seat, mono }); + pub fn set_seat_mono(&self, seat: Seat, mono: bool) { + self.send(&ClientMessage::SetSeatMono { seat, mono }); } pub fn set_env(&self, key: &str, val: &str) { @@ -582,20 +582,20 @@ impl Client { self.i3bar_separator.borrow().clone() } - pub fn set_split(&self, seat: Seat, axis: Axis) { - self.send(&ClientMessage::SetSplit { seat, axis }); + pub fn set_seat_split(&self, seat: Seat, axis: Axis) { + self.send(&ClientMessage::SetSeatSplit { seat, axis }); } - pub fn create_split(&self, seat: Seat, axis: Axis) { - self.send(&ClientMessage::CreateSplit { seat, axis }); + pub fn create_seat_split(&self, seat: Seat, axis: Axis) { + self.send(&ClientMessage::CreateSeatSplit { seat, axis }); } - pub fn close(&self, seat: Seat) { - self.send(&ClientMessage::Close { seat }); + pub fn seat_close(&self, seat: Seat) { + self.send(&ClientMessage::SeatClose { seat }); } - pub fn focus_parent(&self, seat: Seat) { - self.send(&ClientMessage::FocusParent { seat }); + pub fn focus_seat_parent(&self, seat: Seat) { + self.send(&ClientMessage::FocusSeatParent { seat }); } pub fn get_seat(&self, name: &str) -> Seat { @@ -792,13 +792,13 @@ impl Client { } pub fn get_pinned(&self, seat: Seat) -> bool { - let res = self.send_with_response(&ClientMessage::GetFloatPinned { seat }); + let res = self.send_with_response(&ClientMessage::GetSeatFloatPinned { seat }); get_response!(res, false, GetFloatPinned { pinned }); pinned } pub fn set_pinned(&self, seat: Seat, pinned: bool) { - self.send(&ClientMessage::SetFloatPinned { seat, pinned }); + self.send(&ClientMessage::SetSeatFloatPinned { seat, pinned }); } pub fn connector_connected(&self, connector: Connector) -> bool { diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index dd8f82ea..1bf85211 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -129,20 +129,20 @@ pub enum ClientMessage<'a> { rate: i32, delay: i32, }, - GetSplit { + GetSeatSplit { seat: Seat, }, SetStatus { status: &'a str, }, - SetSplit { + SetSeatSplit { seat: Seat, axis: Axis, }, - GetMono { + GetSeatMono { seat: Seat, }, - SetMono { + SetSeatMono { seat: Seat, mono: bool, }, @@ -168,11 +168,11 @@ pub enum ClientMessage<'a> { args: Vec, env: Vec<(String, String)>, }, - Focus { + SeatFocus { seat: Seat, direction: Direction, }, - Move { + SeatMove { seat: Seat, direction: Direction, }, @@ -196,20 +196,20 @@ pub enum ClientMessage<'a> { colorable: Colorable, color: Color, }, - CreateSplit { + CreateSeatSplit { seat: Seat, axis: Axis, }, - Close { + SeatClose { seat: Seat, }, - FocusParent { + FocusSeatParent { seat: Seat, }, - GetFloating { + GetSeatFloating { seat: Seat, }, - SetFloating { + SetSeatFloating { seat: Seat, floating: bool, }, @@ -261,7 +261,7 @@ pub enum ClientMessage<'a> { seat: Seat, workspace: Workspace, }, - SetWorkspace { + SetSeatWorkspace { seat: Seat, workspace: Workspace, }, @@ -280,11 +280,11 @@ pub enum ClientMessage<'a> { key: &'a str, val: &'a str, }, - SetFullscreen { + SetSeatFullscreen { seat: Seat, fullscreen: bool, }, - GetFullscreen { + GetSeatFullscreen { seat: Seat, }, GetDeviceConnectors { @@ -546,10 +546,10 @@ pub enum ClientMessage<'a> { above: bool, }, GetFloatAboveFullscreen, - GetFloatPinned { + GetSeatFloatPinned { seat: Seat, }, - SetFloatPinned { + SetSeatFloatPinned { seat: Seat, pinned: bool, }, diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index 555eb7cf..c6851597 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -259,12 +259,12 @@ impl Seat { /// Moves the keyboard focus of the seat in the specified direction. pub fn focus(self, direction: Direction) { - get!().focus(self, direction) + get!().seat_focus(self, direction) } /// Moves the focused window in the specified direction. pub fn move_(self, direction: Direction) { - get!().move_(self, direction) + get!().seat_move(self, direction) } /// Sets the keymap of the seat. @@ -287,12 +287,12 @@ impl Seat { /// Returns whether the parent-container of the currently focused window is in mono-mode. pub fn mono(self) -> bool { - get!(false).mono(self) + get!(false).seat_mono(self) } /// Sets whether the parent-container of the currently focused window is in mono-mode. pub fn set_mono(self, mono: bool) { - get!().set_mono(self, mono) + get!().set_seat_mono(self, mono) } /// Toggles whether the parent-container of the currently focused window is in mono-mode. @@ -302,12 +302,12 @@ impl Seat { /// Returns the split axis of the parent-container of the currently focused window. pub fn split(self) -> Axis { - get!(Axis::Horizontal).split(self) + get!(Axis::Horizontal).seat_split(self) } /// Sets the split axis of the parent-container of the currently focused window. pub fn set_split(self, axis: Axis) { - get!().set_split(self, axis) + get!().set_seat_split(self, axis) } /// Toggles the split axis of the parent-container of the currently focused window. @@ -322,33 +322,33 @@ impl Seat { /// Creates a new container with the specified split in place of the currently focused window. pub fn create_split(self, axis: Axis) { - get!().create_split(self, axis); + get!().create_seat_split(self, axis); } /// Focuses the parent node of the currently focused window. pub fn focus_parent(self) { - get!().focus_parent(self); + get!().focus_seat_parent(self); } /// Requests the currently focused window to be closed. pub fn close(self) { - get!().close(self); + get!().seat_close(self); } /// Returns whether the currently focused window is floating. pub fn get_floating(self) -> bool { - get!().get_floating(self) + get!().get_seat_floating(self) } /// Sets whether the currently focused window is floating. pub fn set_floating(self, floating: bool) { - get!().set_floating(self, floating); + get!().set_seat_floating(self, floating); } /// Toggles whether the currently focused window is floating. /// /// You can do the same by double-clicking on the header. pub fn toggle_floating(self) { - get!().toggle_floating(self); + get!().toggle_seat_floating(self); } /// Returns the workspace that is currently active on the output that contains the seat's @@ -377,22 +377,22 @@ impl Seat { /// Moves the currently focused window to the workspace. pub fn set_workspace(self, workspace: Workspace) { - get!().set_workspace(self, workspace) + get!().set_seat_workspace(self, workspace) } /// Toggles whether the currently focused window is fullscreen. pub fn toggle_fullscreen(self) { let c = get!(); - c.set_fullscreen(self, !c.get_fullscreen(self)); + c.set_seat_fullscreen(self, !c.get_seat_fullscreen(self)); } /// Returns whether the currently focused window is fullscreen. pub fn fullscreen(self) -> bool { - get!(false).get_fullscreen(self) + get!(false).get_seat_fullscreen(self) } /// Sets whether the currently focused window is fullscreen. pub fn set_fullscreen(self, fullscreen: bool) { - get!().set_fullscreen(self, fullscreen) + get!().set_seat_fullscreen(self, fullscreen) } /// Disables the currently active pointer constraint on this seat. diff --git a/src/config/handler.rs b/src/config/handler.rs index 3caff976..f9152adc 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -299,7 +299,7 @@ impl ConfigProxyHandler { self.state.config.set(Some(Rc::new(config))); } - fn handle_get_fullscreen(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_fullscreen(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetFullscreen { fullscreen: seat.get_fullscreen(), @@ -307,7 +307,7 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_fullscreen(&self, seat: Seat, fullscreen: bool) -> Result<(), CphError> { + fn handle_set_seat_fullscreen(&self, seat: Seat, fullscreen: bool) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.set_fullscreen(fullscreen); Ok(()) @@ -484,19 +484,19 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_close(&self, seat: Seat) -> Result<(), CphError> { + fn handle_seat_close(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.close(); Ok(()) } - fn handle_focus(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { + fn handle_seat_focus(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.move_focus(direction.into()); Ok(()) } - fn handle_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { + fn handle_seat_move(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.move_focused(direction.into()); Ok(()) @@ -843,7 +843,7 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_workspace(&self, seat: Seat, ws: Workspace) -> Result<(), CphError> { + fn handle_set_seat_workspace(&self, seat: Seat, ws: Workspace) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let name = self.get_workspace(ws)?; let workspace = match self.state.workspaces.get(name.deref()) { @@ -1164,7 +1164,7 @@ impl ConfigProxyHandler { } } - fn handle_get_float_pinned(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_float_pinned(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetFloatPinned { pinned: seat.pinned(), @@ -1172,7 +1172,7 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_float_pinned(&self, seat: Seat, pinned: bool) -> Result<(), CphError> { + fn handle_set_seat_float_pinned(&self, seat: Seat, pinned: bool) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.set_pinned(pinned); Ok(()) @@ -1344,7 +1344,7 @@ impl ConfigProxyHandler { } } - fn handle_get_mono(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_mono(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetMono { mono: seat.get_mono().unwrap_or(false), @@ -1352,13 +1352,13 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> { + fn handle_set_seat_mono(&self, seat: Seat, mono: bool) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.set_mono(mono); Ok(()) } - fn handle_get_split(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetSplit { axis: seat @@ -1369,7 +1369,7 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { + fn handle_set_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.set_split(axis.into()); Ok(()) @@ -1472,13 +1472,13 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_create_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { + fn handle_create_seat_split(&self, seat: Seat, axis: Axis) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.create_split(axis.into()); Ok(()) } - fn handle_focus_parent(&self, seat: Seat) -> Result<(), CphError> { + fn handle_focus_seat_parent(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.focus_parent(); Ok(()) @@ -1493,7 +1493,7 @@ impl ConfigProxyHandler { self.state.backend.get().switch_to(vtnr); } - fn handle_get_floating(&self, seat: Seat) -> Result<(), CphError> { + fn handle_get_seat_floating(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetFloating { floating: seat.get_floating().unwrap_or(false), @@ -1501,7 +1501,7 @@ impl ConfigProxyHandler { Ok(()) } - fn handle_set_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { + fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.set_floating(floating); Ok(()) @@ -1751,25 +1751,29 @@ impl ConfigProxyHandler { ClientMessage::SetSeat { device, seat } => { self.handle_set_seat(device, seat).wrn("set_seat")? } - ClientMessage::GetMono { seat } => self.handle_get_mono(seat).wrn("get_mono")?, - ClientMessage::SetMono { seat, mono } => { - self.handle_set_mono(seat, mono).wrn("set_mono")? + ClientMessage::GetSeatMono { seat } => { + self.handle_get_seat_mono(seat).wrn("get_seat_mono")? } - ClientMessage::GetSplit { seat } => self.handle_get_split(seat).wrn("get_split")?, - ClientMessage::SetSplit { seat, axis } => { - self.handle_set_split(seat, axis).wrn("set_split")? + ClientMessage::SetSeatMono { seat, mono } => { + self.handle_set_seat_mono(seat, mono).wrn("set_seat_mono")? } + ClientMessage::GetSeatSplit { seat } => { + self.handle_get_seat_split(seat).wrn("get_seat_split")? + } + ClientMessage::SetSeatSplit { seat, axis } => self + .handle_set_seat_split(seat, axis) + .wrn("set_seat_split")?, ClientMessage::AddShortcut { seat, mods, sym } => self .handle_add_shortcut(seat, Modifiers(!0), mods, sym) .wrn("add_shortcut")?, ClientMessage::RemoveShortcut { seat, mods, sym } => self .handle_remove_shortcut(seat, mods, sym) .wrn("remove_shortcut")?, - ClientMessage::Focus { seat, direction } => { - self.handle_focus(seat, direction).wrn("focus")? + ClientMessage::SeatFocus { seat, direction } => { + self.handle_seat_focus(seat, direction).wrn("seat_focus")? } - ClientMessage::Move { seat, direction } => { - self.handle_move(seat, direction).wrn("move")? + ClientMessage::SeatMove { seat, direction } => { + self.handle_seat_move(seat, direction).wrn("seat_move")? } ClientMessage::GetInputDevices { seat } => self.handle_get_input_devices(seat), ClientMessage::GetSeats => self.handle_get_seats(), @@ -1784,18 +1788,18 @@ impl ConfigProxyHandler { ClientMessage::GetColor { colorable } => { self.handle_get_color(colorable).wrn("get_color")? } - ClientMessage::CreateSplit { seat, axis } => { - self.handle_create_split(seat, axis).wrn("create_split")? - } - ClientMessage::FocusParent { seat } => { - self.handle_focus_parent(seat).wrn("focus_parent")? - } - ClientMessage::GetFloating { seat } => { - self.handle_get_floating(seat).wrn("get_floating")? - } - ClientMessage::SetFloating { seat, floating } => self - .handle_set_floating(seat, floating) - .wrn("set_floating")?, + ClientMessage::CreateSeatSplit { seat, axis } => self + .handle_create_seat_split(seat, axis) + .wrn("create_seat_split")?, + ClientMessage::FocusSeatParent { seat } => self + .handle_focus_seat_parent(seat) + .wrn("focus_seat_parent")?, + ClientMessage::GetSeatFloating { seat } => self + .handle_get_seat_floating(seat) + .wrn("get_seat_floating")?, + ClientMessage::SetSeatFloating { seat, floating } => self + .handle_set_seat_floating(seat, floating) + .wrn("set_seat_floating")?, ClientMessage::Quit => self.handle_quit(), ClientMessage::SwitchTo { vtnr } => self.handle_switch_to(vtnr), ClientMessage::HasCapability { device, cap } => self @@ -1823,9 +1827,9 @@ impl ConfigProxyHandler { ClientMessage::ShowWorkspace { seat, workspace } => self .handle_show_workspace(seat, workspace) .wrn("show_workspace")?, - ClientMessage::SetWorkspace { seat, workspace } => self - .handle_set_workspace(seat, workspace) - .wrn("set_workspace")?, + ClientMessage::SetSeatWorkspace { seat, workspace } => self + .handle_set_seat_workspace(seat, workspace) + .wrn("set_seat_workspace")?, ClientMessage::GetConnector { ty, idx } => { self.handle_get_connector(ty, idx).wrn("get_connector")? } @@ -1844,7 +1848,7 @@ impl ConfigProxyHandler { ClientMessage::ConnectorSetEnabled { connector, enabled } => self .handle_connector_set_enabled(connector, enabled) .wrn("connector_set_enabled")?, - ClientMessage::Close { seat } => self.handle_close(seat).wrn("close")?, + ClientMessage::SeatClose { seat } => self.handle_seat_close(seat).wrn("seat_close")?, ClientMessage::SetStatus { status } => self.handle_set_status(status), ClientMessage::GetTimer { name } => self.handle_get_timer(name).wrn("get_timer")?, ClientMessage::RemoveTimer { timer } => { @@ -1858,12 +1862,12 @@ impl ConfigProxyHandler { .handle_program_timer(timer, initial, periodic) .wrn("program_timer")?, ClientMessage::SetEnv { key, val } => self.handle_set_env(key, val), - ClientMessage::SetFullscreen { seat, fullscreen } => self - .handle_set_fullscreen(seat, fullscreen) - .wrn("set_fullscreen")?, - ClientMessage::GetFullscreen { seat } => { - self.handle_get_fullscreen(seat).wrn("get_fullscreen")? - } + ClientMessage::SetSeatFullscreen { seat, fullscreen } => self + .handle_set_seat_fullscreen(seat, fullscreen) + .wrn("set_seat_fullscreen")?, + ClientMessage::GetSeatFullscreen { seat } => self + .handle_get_seat_fullscreen(seat) + .wrn("get_seat_fullscreen")?, ClientMessage::Reload => self.handle_reload(), ClientMessage::GetDeviceConnectors { device } => self .handle_get_connectors(Some(device), false) @@ -2111,12 +2115,12 @@ impl ConfigProxyHandler { self.handle_set_float_above_fullscreen(above) } ClientMessage::GetFloatAboveFullscreen => self.handle_get_float_above_fullscreen(), - ClientMessage::GetFloatPinned { seat } => { - self.handle_get_float_pinned(seat).wrn("get_float_pinned")? - } - ClientMessage::SetFloatPinned { seat, pinned } => self - .handle_set_float_pinned(seat, pinned) - .wrn("set_float_pinned")?, + ClientMessage::GetSeatFloatPinned { seat } => self + .handle_get_seat_float_pinned(seat) + .wrn("get_seat_float_pinned")?, + ClientMessage::SetSeatFloatPinned { seat, pinned } => self + .handle_set_seat_float_pinned(seat, pinned) + .wrn("set_seat_float_pinned")?, ClientMessage::SetShowFloatPinIcon { show } => { self.handle_set_show_float_pin_icon(show) } diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 2391b886..12c086d7 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -214,14 +214,14 @@ impl TestConfig { } pub fn create_split(&self, seat: SeatId, axis: Axis) -> TestResult { - self.send(ClientMessage::CreateSplit { + self.send(ClientMessage::CreateSeatSplit { seat: Seat(seat.raw() as _), axis, }) } pub fn set_mono(&self, seat: SeatId, mono: bool) -> TestResult { - self.send(ClientMessage::SetMono { + self.send(ClientMessage::SetSeatMono { seat: Seat(seat.raw() as _), mono, }) @@ -248,14 +248,14 @@ impl TestConfig { } pub fn focus(&self, seat: SeatId, direction: Direction) -> TestResult { - self.send(ClientMessage::Focus { + self.send(ClientMessage::SeatFocus { seat: Seat(seat.raw() as _), direction, }) } pub fn set_fullscreen(&self, seat: SeatId, fs: bool) -> TestResult { - self.send(ClientMessage::SetFullscreen { + self.send(ClientMessage::SetSeatFullscreen { seat: Seat(seat.raw() as _), fullscreen: fs, }) @@ -270,7 +270,7 @@ impl TestConfig { } pub fn set_floating(&self, seat: SeatId, floating: bool) -> TestResult { - self.send(ClientMessage::SetFloating { + self.send(ClientMessage::SetSeatFloating { seat: Seat(seat.raw() as _), floating, }) From ab095b89cf7f8c3c240d1ca573b9f2d8afd9417c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 2 May 2025 19:24:43 +0200 Subject: [PATCH 06/35] config: add Client --- jay-config/src/_private/client.rs | 43 ++++++++++++++++++++++++------- jay-config/src/_private/ipc.rs | 20 ++++++++++++++ jay-config/src/client.rs | 36 ++++++++++++++++++++++++++ jay-config/src/lib.rs | 1 + src/client.rs | 1 - src/config/handler.rs | 43 +++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 jay-config/src/client.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index fd105478..0b7542f4 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -10,6 +10,7 @@ use { logging, }, Axis, Direction, ModifiedKeySym, PciId, Workspace, + client::Client, exec::Command, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, @@ -81,7 +82,7 @@ struct KeyHandler { latched: Vec>, } -pub(crate) struct Client { +pub(crate) struct ConfigClient { configure: extern "C" fn(), srv_data: *const u8, srv_unref: unsafe extern "C" fn(data: *const u8), @@ -145,7 +146,7 @@ struct Task { waker: Waker, } -impl Drop for Client { +impl Drop for ConfigClient { fn drop(&mut self) { unsafe { (self.srv_unref)(self.srv_data); @@ -154,13 +155,13 @@ impl Drop for Client { } thread_local! { - pub(crate) static CLIENT: Cell<*const Client> = const { Cell::new(ptr::null()) }; + pub(crate) static CLIENT: Cell<*const ConfigClient> = const { Cell::new(ptr::null()) }; } -unsafe fn with_client T>(data: *const u8, f: F) -> T { +unsafe fn with_client T>(data: *const u8, f: F) -> T { struct Reset<'a> { - cell: &'a Cell<*const Client>, - val: *const Client, + cell: &'a Cell<*const ConfigClient>, + val: *const ConfigClient, } impl Drop for Reset<'_> { fn drop(&mut self) { @@ -168,7 +169,7 @@ unsafe fn with_client T>(data: *const u8, f: F) -> T { } } CLIENT.with(|cell| unsafe { - let client = data as *const Client; + let client = data as *const ConfigClient; Rc::increment_strong_count(client); let client = Rc::from_raw(client); let old = cell.replace(client.deref()); @@ -214,7 +215,7 @@ pub unsafe extern "C" fn init( size: usize, f: extern "C" fn(), ) -> *const u8 { - let client = Rc::new(Client { + let client = Rc::new(ConfigClient { configure: f, srv_data, srv_unref, @@ -251,7 +252,7 @@ pub unsafe extern "C" fn init( } pub unsafe extern "C" fn unref(data: *const u8) { - let client = data as *const Client; + let client = data as *const ConfigClient; unsafe { drop(Rc::from_raw(client)); } @@ -278,7 +279,7 @@ macro_rules! get_response { } } -impl Client { +impl ConfigClient { fn send(&self, msg: &ClientMessage) { let mut buf = self.bufs.borrow_mut().pop().unwrap_or_default(); buf.clear(); @@ -1259,6 +1260,28 @@ impl Client { } } + pub fn clients(&self) -> Vec { + let res = self.send_with_response(&ClientMessage::GetClients); + get_response!(res, vec!(), GetClients { clients }); + clients + } + + pub fn client_exists(&self, client: Client) -> bool { + let res = self.send_with_response(&ClientMessage::ClientExists { client }); + get_response!(res, false, ClientExists { exists }); + exists + } + + pub fn client_is_xwayland(&self, client: Client) -> bool { + let res = self.send_with_response(&ClientMessage::ClientIsXwayland { client }); + get_response!(res, false, ClientIsXwayland { is_xwayland }); + is_xwayland + } + + pub fn client_kill(&self, client: Client) { + self.send(&ClientMessage::ClientKill { client }); + } + fn handle_msg(&self, msg: &[u8]) { self.handle_msg2(msg); self.dispatch_futures(); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 1bf85211..03eb85e3 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -2,6 +2,7 @@ use { crate::{ _private::{PollableId, WireMode}, Axis, Direction, PciId, Workspace, + client::Client, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, capability::Capability, @@ -565,6 +566,16 @@ pub enum ClientMessage<'a> { GetConnectorWorkspaces { connector: Connector, }, + GetClients, + ClientExists { + client: Client, + }, + ClientKill { + client: Client, + }, + ClientIsXwayland { + client: Client, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -728,6 +739,15 @@ pub enum Response { GetConnectorWorkspaces { workspaces: Vec, }, + GetClients { + clients: Vec, + }, + ClientExists { + exists: bool, + }, + ClientIsXwayland { + is_xwayland: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs new file mode 100644 index 00000000..5e908352 --- /dev/null +++ b/jay-config/src/client.rs @@ -0,0 +1,36 @@ +//! Tools for inspecting and manipulating clients. + +use serde::{Deserialize, Serialize}; + +/// A client connected to the compositor. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Client(pub u64); + +impl Client { + /// Returns whether the client exists. + pub fn exists(self) -> bool { + self.0 != 0 && get!(false).client_exists(self) + } + + /// Returns whether the client does not exist. + /// + /// This is a shorthand for `!self.exists()`. + pub fn does_not_exist(self) -> bool { + !self.exists() + } + + /// Returns whether this client is XWayland. + pub fn is_xwayland(self) -> bool { + get!(false).client_is_xwayland(self) + } + + /// Disconnects the client. + pub fn kill(self) { + get!().client_kill(self) + } +} + +/// Returns all current clients. +pub fn clients() -> Vec { + get!().clients() +} diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index dbcd2e85..b0570033 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -58,6 +58,7 @@ use { mod macros; #[doc(hidden)] pub mod _private; +pub mod client; pub mod embedded; pub mod exec; pub mod input; diff --git a/src/client.rs b/src/client.rs index 3781cd49..f925a2ea 100644 --- a/src/client.rs +++ b/src/client.rs @@ -106,7 +106,6 @@ impl Clients { ClientId(self.next_client_id.fetch_add(1)) } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn get(&self, id: ClientId) -> Result, ClientError> { let clients = self.clients.borrow(); match clients.get(&id) { diff --git a/src/config/handler.rs b/src/config/handler.rs index f9152adc..212c8041 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -5,6 +5,7 @@ use { self, BackendColorSpace, BackendTransferFunction, ConnectorId, DrmDeviceId, InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, }, + client::{Client, ClientId}, cmm::cmm_transfer_function::TransferFunction, compositor::MAX_EXTENTS, config::ConfigProxy, @@ -38,6 +39,7 @@ use { ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, + client::Client as ConfigClient, input::{ FocusFollowsMouseMode, InputDevice, Seat, acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile}, @@ -1717,6 +1719,39 @@ impl ConfigProxyHandler { self.keymaps.remove(&keymap); } + fn get_client(&self, client: ConfigClient) -> Result, CphError> { + self.state + .clients + .get(ClientId::from_raw(client.0)) + .ok() + .ok_or(CphError::ClientDoesNotExist(client)) + } + + fn handle_get_clients(&self) { + let mut clients = vec![]; + for client in self.state.clients.clients.borrow().values() { + clients.push(ConfigClient(client.data.id.raw())); + } + self.respond(Response::GetClients { clients }); + } + + fn handle_client_exists(&self, client: ConfigClient) { + self.respond(Response::ClientExists { + exists: self.get_client(client).is_ok(), + }); + } + + fn handle_client_is_xwayland(&self, client: ConfigClient) -> Result<(), CphError> { + self.respond(Response::ClientIsXwayland { + is_xwayland: self.get_client(client)?.is_xwayland, + }); + Ok(()) + } + + fn handle_client_kill(&self, client: ConfigClient) { + self.state.clients.kill(ClientId::from_raw(client.0)); + } + pub fn handle_request(self: &Rc, msg: &[u8]) { if let Err(e) = self.handle_request_(msg) { log::error!("Could not handle client request: {}", ErrorFmt(e)); @@ -2130,6 +2165,12 @@ impl ConfigProxyHandler { ClientMessage::GetConnectorWorkspaces { connector } => self .handle_get_connector_workspaces(connector) .wrn("get_connector_workspaces")?, + ClientMessage::GetClients => self.handle_get_clients(), + ClientMessage::ClientExists { client } => self.handle_client_exists(client), + ClientMessage::ClientIsXwayland { client } => self + .handle_client_is_xwayland(client) + .wrn("client_is_xwayland")?, + ClientMessage::ClientKill { client } => self.handle_client_kill(client), } Ok(()) } @@ -2205,6 +2246,8 @@ enum CphError { UnknownColorSpace(ColorSpace), #[error("Unknown transfer function {0:?}")] UnknownTransferFunction(ConfigTransferFunction), + #[error("Client {0:?} does not exist")] + ClientDoesNotExist(ConfigClient), } trait WithRequestName { From 9977f9dfdfe6ac495559b86857d444885e784dc9 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 29 Apr 2025 15:29:39 +0200 Subject: [PATCH 07/35] config: add Window --- jay-config/src/_private/client.rs | 137 +++++++ jay-config/src/_private/ipc.rs | 136 +++++++ jay-config/src/input.rs | 15 + jay-config/src/keyboard/mods.rs | 82 ++-- jay-config/src/lib.rs | 12 +- jay-config/src/macros.rs | 123 ++++-- jay-config/src/window.rs | 204 ++++++++++ src/config.rs | 16 +- src/config/handler.rs | 369 +++++++++++++++++- src/ifs/wl_seat.rs | 86 +--- src/ifs/wl_surface/x_surface/xwindow.rs | 7 +- .../wl_surface/xdg_surface/xdg_toplevel.rs | 8 +- src/state.rs | 7 + src/tree/container.rs | 17 +- src/tree/float.rs | 4 +- src/tree/placeholder.rs | 19 +- src/tree/toplevel.rs | 111 +++++- src/utils/clonecell.rs | 15 +- src/utils/toplevel_identifier.rs | 7 +- 19 files changed, 1172 insertions(+), 203 deletions(-) create mode 100644 jay-config/src/window.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 0b7542f4..7a98bc94 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -30,6 +30,7 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, + window::{Window, WindowType}, xwayland::XScalingMode, }, bincode::Options, @@ -342,6 +343,74 @@ impl ConfigClient { self.send(&ClientMessage::SeatMove { seat, direction }); } + pub fn window_move(&self, window: Window, direction: Direction) { + self.send(&ClientMessage::WindowMove { window, direction }); + } + + pub fn window_exists(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::WindowExists { window }); + get_response!(res, false, WindowExists { exists }); + exists + } + + pub fn window_client(&self, window: Window) -> Client { + let res = self.send_with_response(&ClientMessage::GetWindowClient { window }); + get_response!(res, Client(0), GetWindowClient { client }); + client + } + + pub fn get_workspace_window(&self, workspace: Workspace) -> Window { + let res = self.send_with_response(&ClientMessage::GetWorkspaceWindow { workspace }); + get_response!(res, Window(0), GetWorkspaceWindow { window }); + window + } + + pub fn get_seat_keyboard_window(&self, seat: Seat) -> Window { + let res = self.send_with_response(&ClientMessage::GetSeatKeyboardWindow { seat }); + get_response!(res, Window(0), GetSeatKeyboardWindow { window }); + window + } + + pub fn focus_window(&self, seat: Seat, window: Window) { + self.send(&ClientMessage::SeatFocusWindow { seat, window }); + } + + pub fn window_title(&self, window: Window) -> String { + let res = self.send_with_response(&ClientMessage::GetWindowTitle { window }); + get_response!(res, String::new(), GetWindowTitle { title }); + title + } + + pub fn window_type(&self, window: Window) -> WindowType { + let res = self.send_with_response(&ClientMessage::GetWindowType { window }); + get_response!(res, WindowType(0), GetWindowType { 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 }); + id + } + + pub fn window_parent(&self, window: Window) -> Window { + let res = self.send_with_response(&ClientMessage::GetWindowParent { window }); + get_response!(res, Window(0), GetWindowParent { window }); + window + } + + pub fn window_children(&self, window: Window) -> Vec { + let res = self.send_with_response(&ClientMessage::GetWindowChildren { window }); + get_response!(res, vec![], GetWindowChildren { windows }); + windows + } + + pub fn window_is_visible(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowIsVisible { window }); + get_response!(res, false, GetWindowIsVisible { visible }); + visible + } + pub fn unbind>(&self, seat: Seat, mod_sym: T) { let mod_sym = mod_sym.into(); if let Entry::Occupied(mut oe) = self.key_handlers.borrow_mut().entry((seat, mod_sym)) { @@ -374,6 +443,12 @@ impl ConfigClient { mono } + pub fn window_mono(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowMono { window }); + get_response!(res, false, GetWindowMono { mono }); + mono + } + pub fn get_timer(&self, name: &str) -> Timer { let res = self.send_with_response(&ClientMessage::GetTimer { name }); get_response!(res, Timer(0), GetTimer { timer }); @@ -421,6 +496,12 @@ impl ConfigClient { workspace } + pub fn get_window_workspace(&self, window: Window) -> Workspace { + let res = self.send_with_response(&ClientMessage::GetWindowWorkspace { window }); + get_response!(res, Workspace(0), GetWindowWorkspace { workspace }); + workspace + } + pub fn get_seat_keyboard_workspace(&self, seat: Seat) -> Workspace { let res = self.send_with_response(&ClientMessage::GetSeatKeyboardWorkspace { seat }); get_response!(res, Workspace(0), GetSeatKeyboardWorkspace { workspace }); @@ -455,12 +536,22 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatWorkspace { seat, workspace }); } + pub fn set_window_workspace(&self, window: Window, workspace: Workspace) { + self.send(&ClientMessage::SetWindowWorkspace { window, workspace }); + } + pub fn seat_split(&self, seat: Seat) -> Axis { let res = self.send_with_response(&ClientMessage::GetSeatSplit { seat }); get_response!(res, Axis::Horizontal, GetSplit { axis }); axis } + pub fn window_split(&self, window: Window) -> Axis { + let res = self.send_with_response(&ClientMessage::GetWindowSplit { window }); + get_response!(res, Axis::Horizontal, GetWindowSplit { axis }); + axis + } + pub fn disable_pointer_constraint(&self, seat: Seat) { self.send(&ClientMessage::DisablePointerConstraint { seat }); } @@ -482,6 +573,16 @@ impl ConfigClient { fullscreen } + pub fn set_window_fullscreen(&self, window: Window, fullscreen: bool) { + self.send(&ClientMessage::SetWindowFullscreen { window, fullscreen }); + } + + pub fn get_window_fullscreen(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFullscreen { window }); + get_response!(res, false, GetWindowFullscreen { fullscreen }); + fullscreen + } + pub fn reset_font(&self) { self.send(&ClientMessage::ResetFont); } @@ -510,6 +611,16 @@ impl ConfigClient { self.set_seat_floating(seat, !self.get_seat_floating(seat)); } + pub fn get_window_floating(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFloating { window }); + get_response!(res, false, GetWindowFloating { floating }); + floating + } + + pub fn set_window_floating(&self, window: Window, floating: bool) { + self.send(&ClientMessage::SetWindowFloating { window, floating }); + } + pub fn reset_colors(&self) { self.send(&ClientMessage::ResetColors); } @@ -553,6 +664,10 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatMono { seat, mono }); } + pub fn set_window_mono(&self, window: Window, mono: bool) { + self.send(&ClientMessage::SetWindowMono { window, mono }); + } + pub fn set_env(&self, key: &str, val: &str) { self.send(&ClientMessage::SetEnv { key, val }); } @@ -587,14 +702,26 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatSplit { seat, axis }); } + pub fn set_window_split(&self, window: Window, axis: Axis) { + self.send(&ClientMessage::SetWindowSplit { window, axis }); + } + pub fn create_seat_split(&self, seat: Seat, axis: Axis) { self.send(&ClientMessage::CreateSeatSplit { seat, axis }); } + pub fn create_window_split(&self, window: Window, axis: Axis) { + self.send(&ClientMessage::CreateWindowSplit { window, axis }); + } + pub fn seat_close(&self, seat: Seat) { self.send(&ClientMessage::SeatClose { seat }); } + pub fn close_window(&self, window: Window) { + self.send(&ClientMessage::WindowClose { window }); + } + pub fn focus_seat_parent(&self, seat: Seat) { self.send(&ClientMessage::FocusSeatParent { seat }); } @@ -802,6 +929,16 @@ impl ConfigClient { self.send(&ClientMessage::SetSeatFloatPinned { seat, pinned }); } + pub fn get_window_pinned(&self, window: Window) -> bool { + let res = self.send_with_response(&ClientMessage::GetWindowFloatPinned { window }); + get_response!(res, false, GetWindowFloatPinned { pinned }); + pinned + } + + pub fn set_window_pinned(&self, window: Window, pinned: bool) { + self.send(&ClientMessage::SetWindowFloatPinned { window, pinned }); + } + pub fn connector_connected(&self, connector: Connector) -> bool { let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector }); get_response!(res, false, ConnectorConnected { connected }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 03eb85e3..07b66d54 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -15,6 +15,7 @@ use { ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode, connector_type::ConnectorType, }, + window::{Window, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -576,6 +577,93 @@ pub enum ClientMessage<'a> { ClientIsXwayland { client: Client, }, + WindowExists { + window: Window, + }, + GetWindowClient { + window: Window, + }, + GetWorkspaceWindow { + workspace: Workspace, + }, + GetSeatKeyboardWindow { + seat: Seat, + }, + SeatFocusWindow { + seat: Seat, + window: Window, + }, + GetWindowTitle { + window: Window, + }, + GetWindowType { + window: Window, + }, + GetWindowId { + window: Window, + }, + GetWindowIsVisible { + window: Window, + }, + GetWindowParent { + window: Window, + }, + GetWindowWorkspace { + window: Window, + }, + GetWindowChildren { + window: Window, + }, + GetWindowSplit { + window: Window, + }, + SetWindowSplit { + window: Window, + axis: Axis, + }, + GetWindowMono { + window: Window, + }, + SetWindowMono { + window: Window, + mono: bool, + }, + WindowMove { + window: Window, + direction: Direction, + }, + CreateWindowSplit { + window: Window, + axis: Axis, + }, + WindowClose { + window: Window, + }, + GetWindowFloating { + window: Window, + }, + SetWindowFloating { + window: Window, + floating: bool, + }, + SetWindowWorkspace { + window: Window, + workspace: Workspace, + }, + SetWindowFullscreen { + window: Window, + fullscreen: bool, + }, + GetWindowFullscreen { + window: Window, + }, + GetWindowFloatPinned { + window: Window, + }, + SetWindowFloatPinned { + window: Window, + pinned: bool, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -748,6 +836,54 @@ pub enum Response { ClientIsXwayland { is_xwayland: bool, }, + WindowExists { + exists: bool, + }, + GetWindowClient { + client: Client, + }, + GetSeatKeyboardWindow { + window: Window, + }, + GetWorkspaceWindow { + window: Window, + }, + GetWindowParent { + window: Window, + }, + GetWindowChildren { + windows: Vec, + }, + GetWindowTitle { + title: String, + }, + GetWindowType { + kind: WindowType, + }, + GetWindowId { + id: String, + }, + GetWindowWorkspace { + workspace: Workspace, + }, + GetWindowFloating { + floating: bool, + }, + GetWindowSplit { + axis: Axis, + }, + GetWindowMono { + mono: bool, + }, + GetWindowFullscreen { + fullscreen: bool, + }, + GetWindowFloatPinned { + pinned: bool, + }, + GetWindowIsVisible { + visible: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index c6851597..0a6045dc 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -10,6 +10,7 @@ use { input::{acceleration::AccelProfile, capability::Capability}, keyboard::{Keymap, mods::Modifiers}, video::Connector, + window::Window, }, serde::{Deserialize, Serialize}, std::time::Duration, @@ -478,6 +479,20 @@ impl Seat { pub fn toggle_float_pinned(self) { self.set_float_pinned(!self.float_pinned()); } + + /// Returns the focused window. + /// + /// If no window is focused, [`Window::exists`] returns false. + pub fn window(self) -> Window { + get!(Window(0)).get_seat_keyboard_window(self) + } + + /// Puts the keyboard focus on the window. + /// + /// This has no effect if the window is not visible. + pub fn focus_window(self, window: Window) { + get!().focus_window(self, window) + } } /// A focus-follows-mouse mode. diff --git a/jay-config/src/keyboard/mods.rs b/jay-config/src/keyboard/mods.rs index 5e98da84..6568ed2e 100644 --- a/jay-config/src/keyboard/mods.rs +++ b/jay-config/src/keyboard/mods.rs @@ -3,35 +3,42 @@ use { crate::{ModifiedKeySym, keyboard::syms::KeySym}, serde::{Deserialize, Serialize}, - std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}, + std::ops::BitOr, }; -/// Zero or more keyboard modifiers -#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash, Debug)] -pub struct Modifiers(pub u32); +bitflags! { + /// Zero or more keyboard modifiers + #[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Default, Hash)] + pub struct Modifiers(pub u32) { + /// The Shift modifier + pub const SHIFT = 1 << 0, + /// The CapsLock modifier. + pub const LOCK = 1 << 1, + /// The Ctrl modifier. + pub const CTRL = 1 << 2, + /// The Mod1 modifier, i.e., Alt. + pub const MOD1 = 1 << 3, + /// The Mod2 modifier, i.e., NumLock. + pub const MOD2 = 1 << 4, + /// The Mod3 modifier. + pub const MOD3 = 1 << 5, + /// The Mod4 modifier, i.e., Logo. + pub const MOD4 = 1 << 6, + /// The Mod5 modifier. + pub const MOD5 = 1 << 7, + + /// Synthetic modifier matching key release events. + /// + /// This can be used to execute a callback on key release. + pub const RELEASE = 1 << 31, + } +} impl Modifiers { /// No modifiers. pub const NONE: Self = Modifiers(0); } -/// The Shift modifier -pub const SHIFT: Modifiers = Modifiers(1 << 0); -/// The CapsLock modifier. -pub const LOCK: Modifiers = Modifiers(1 << 1); -/// The Ctrl modifier. -pub const CTRL: Modifiers = Modifiers(1 << 2); -/// The Mod1 modifier, i.e., Alt. -pub const MOD1: Modifiers = Modifiers(1 << 3); -/// The Mod2 modifier, i.e., NumLock. -pub const MOD2: Modifiers = Modifiers(1 << 4); -/// The Mod3 modifier. -pub const MOD3: Modifiers = Modifiers(1 << 5); -/// The Mod4 modifier, i.e., Logo. -pub const MOD4: Modifiers = Modifiers(1 << 6); -/// The Mod5 modifier. -pub const MOD5: Modifiers = Modifiers(1 << 7); - /// Alias for `LOCK`. pub const CAPS: Modifiers = LOCK; /// Alias for `MOD1`. @@ -41,19 +48,6 @@ pub const NUM: Modifiers = MOD2; /// Alias for `MOD4`. pub const LOGO: Modifiers = MOD4; -/// Synthetic modifier matching key release events. -/// -/// This can be used to execute a callback on key release. -pub const RELEASE: Modifiers = Modifiers(1 << 31); - -impl BitOr for Modifiers { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - Self(self.0 | rhs.0) - } -} - impl BitOr for Modifiers { type Output = ModifiedKeySym; @@ -64,23 +58,3 @@ impl BitOr for Modifiers { } } } - -impl BitAnd for Modifiers { - type Output = Self; - - fn bitand(self, rhs: Self) -> Self::Output { - Self(self.0 & rhs.0) - } -} - -impl BitOrAssign for Modifiers { - fn bitor_assign(&mut self, rhs: Self) { - self.0 |= rhs.0 - } -} - -impl BitAndAssign for Modifiers { - fn bitand_assign(&mut self, rhs: Self) { - self.0 &= rhs.0 - } -} diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index b0570033..39b6f6cc 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -46,7 +46,9 @@ #[expect(unused_imports)] use crate::input::Seat; use { - crate::{_private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector}, + crate::{ + _private::ipc::WorkspaceSource, keyboard::ModifiedKeySym, video::Connector, window::Window, + }, serde::{Deserialize, Serialize}, std::{ fmt::{Debug, Display, Formatter}, @@ -70,6 +72,7 @@ pub mod tasks; pub mod theme; pub mod timer; pub mod video; +pub mod window; pub mod xwayland; /// A planar direction. @@ -174,6 +177,13 @@ impl Workspace { pub fn move_to_output(self, output: Connector) { get!().move_to_output(WorkspaceSource::Explicit(self), output); } + + /// Returns the root container of this workspace. + /// + /// If no such container exists, [`Window::exists`] returns false. + pub fn window(self) -> Window { + get!(Window(0)).get_workspace_window(self) + } } /// Returns the workspace with the given name. diff --git a/jay-config/src/macros.rs b/jay-config/src/macros.rs index 012d8d79..03b87581 100644 --- a/jay-config/src/macros.rs +++ b/jay-config/src/macros.rs @@ -43,40 +43,89 @@ macro_rules! get { }}; } -// #[macro_export] -// macro_rules! log { -// ($lvl:expr, $($arg:tt)+) => ({ -// $crate::log( -// $lvl, -// &format!($($args)*), -// ); -// }) -// } -// -// #[macro_export] -// macro_rules! trace { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Trace, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! debug { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Debug, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! info { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Info, $($arg)+) -// } -// } -// -// #[macro_export] -// macro_rules! info { -// ($($arg:tt)+) => { -// $crate::log!($crate::LogLevel::Info, $($arg)+) -// } -// } +macro_rules! bitflags { + ( + $(#[$attr1:meta])* + $vis1:vis struct $name:ident($vis2:vis $rep:ty) { + $( + $(#[$attr2:meta])* + $vis3:vis const $var:ident = $val:expr, + )* + } + ) => { + $(#[$attr1])* + $vis1 struct $name($vis2 $rep); + + $( + $(#[$attr2])* + $vis3 const $var: $name = $name($val); + )* + + impl std::ops::BitOr for $name { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } + } + + impl std::ops::BitAnd for $name { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } + } + + impl std::ops::BitOrAssign for $name { + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } + } + + impl std::ops::BitAndAssign for $name { + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } + } + + impl std::ops::BitXorAssign for $name { + fn bitxor_assign(&mut self, rhs: Self) { + self.0 ^= rhs.0; + } + } + + impl std::ops::Not for $name { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } + } + + impl std::fmt::Debug for $name { + #[allow(clippy::allow_attributes, clippy::bad_bit_mask, unused_mut)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut any = false; + let mut v = self.0; + $( + if $val != 0 && v & $val == $val { + if any { + write!(f, "|")?; + } + any = true; + write!(f, "{}", stringify!($var))?; + v &= !$val; + } + )* + if !any || v != 0 { + if any { + write!(f, "|")?; + } + write!(f, "0x{:x}", v)?; + } + Ok(()) + } + } + } +} diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs new file mode 100644 index 00000000..ebf35122 --- /dev/null +++ b/jay-config/src/window.rs @@ -0,0 +1,204 @@ +//! Tools for inspecting and manipulating windows. + +use { + crate::{Axis, Direction, Workspace, client::Client}, + serde::{Deserialize, Serialize}, +}; + +/// A toplevel window. +/// +/// A toplevel window is anything that can be stored within a container tile or within a +/// floating window. +/// +/// There are currently four types of windows: +/// +/// - Containers +/// - Placeholders that take the place of a window when it goes fullscreen +/// - XDG toplevels +/// - X windows +/// +/// You can find out the type of a window by using the [`Window::type_`] function. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct Window(pub u64); + +bitflags! { + /// The type of a window. + #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)] + pub struct WindowType(pub u64) { + /// A container. + pub const CONTAINER = 1 << 0, + /// A placeholder. + pub const PLACEHOLDER = 1 << 1, + /// An XDG toplevel. + pub const XDG_TOPLEVEL = 1 << 2, + /// An X window. + pub const X_WINDOW = 1 << 3, + } +} + +/// A window created by a client. +/// +/// This is the same as `XDG_TOPLEVEL | X_WINDOW`. +pub const CLIENT_WINDOW: WindowType = WindowType(XDG_TOPLEVEL.0 | X_WINDOW.0); + +impl Window { + /// Returns whether the window exists. + pub fn exists(self) -> bool { + self.0 != 0 && get!(false).window_exists(self) + } + + /// Returns whether the window does not exist. + /// + /// This is a shorthand for `!self.exists()`. + pub fn does_not_exist(self) -> bool { + !self.exists() + } + + /// Returns the client of the window. + /// + /// If the window does not have a client, [`Client::exists`] return false. + pub fn client(self) -> Client { + get!(Client(0)).window_client(self) + } + + /// Returns the title of the window. + pub fn title(self) -> String { + get!().window_title(self) + } + + /// Returns the type of the window. + pub fn type_(self) -> WindowType { + get!(WindowType(0)).window_type(self) + } + + /// Returns the identifier of the window. + /// + /// This is the identifier used in the `ext-foreign-toplevel-list-v1` protocol. + pub fn id(self) -> String { + get!().window_id(self) + } + + /// Returns whether this window is visible. + pub fn is_visible(self) -> bool { + get!().window_is_visible(self) + } + + /// Returns the parent of this window. + /// + /// If this window has no parent, [`Window::exists`] returns false. + pub fn parent(self) -> Window { + get!(Window(0)).window_parent(self) + } + + /// Returns the children of this window. + /// + /// Only containers have children. + pub fn children(self) -> Vec { + get!().window_children(self) + } + + /// Moves the window in the specified direction. + pub fn move_(self, direction: Direction) { + get!().window_move(self, direction) + } + + /// Returns whether the parent-container of the window is in mono-mode. + pub fn mono(self) -> bool { + get!(false).window_mono(self) + } + + /// Sets whether the parent-container of the window is in mono-mode. + pub fn set_mono(self, mono: bool) { + get!().set_window_mono(self, mono) + } + + /// Toggles whether the parent-container of the window is in mono-mode. + pub fn toggle_mono(self) { + self.set_mono(!self.mono()); + } + + /// Returns the split axis of the parent-container of the window. + pub fn split(self) -> Axis { + get!(Axis::Horizontal).window_split(self) + } + + /// Sets the split axis of the parent-container of the window. + pub fn set_split(self, axis: Axis) { + get!().set_window_split(self, axis) + } + + /// Toggles the split axis of the parent-container of the window. + pub fn toggle_split(self) { + self.set_split(self.split().other()); + } + + /// Creates a new container with the specified split in place of the window. + pub fn create_split(self, axis: Axis) { + get!().create_window_split(self, axis); + } + + /// Requests the window to be closed. + pub fn close(self) { + get!().close_window(self); + } + + /// Returns whether the window is floating. + pub fn floating(self) -> bool { + get!().get_window_floating(self) + } + /// Sets whether the window is floating. + pub fn set_floating(self, floating: bool) { + get!().set_window_floating(self, floating); + } + + /// Toggles whether the window is floating. + /// + /// You can do the same by double-clicking on the header. + pub fn toggle_floating(self) { + self.set_floating(!self.floating()); + } + + /// Returns the workspace that this window belongs to. + /// + /// If no such workspace exists, `exists` returns `false` for the returned workspace. + pub fn workspace(self) -> Workspace { + get!(Workspace(0)).get_window_workspace(self) + } + + /// Moves the window to the workspace. + pub fn set_workspace(self, workspace: Workspace) { + get!().set_window_workspace(self, workspace) + } + + /// Toggles whether the currently focused window is fullscreen. + pub fn toggle_fullscreen(self) { + self.set_fullscreen(!self.fullscreen()) + } + /// Returns whether the window is fullscreen. + pub fn fullscreen(self) -> bool { + get!(false).get_window_fullscreen(self) + } + + /// Sets whether the window is fullscreen. + pub fn set_fullscreen(self, fullscreen: bool) { + get!().set_window_fullscreen(self, fullscreen) + } + + /// Gets whether the window is pinned. + /// + /// If a floating window is pinned, it will stay visible even when switching to a + /// different workspace. + pub fn float_pinned(self) -> bool { + get!().get_window_pinned(self) + } + + /// Sets whether the window is pinned. + pub fn set_float_pinned(self, pinned: bool) { + get!().set_window_pinned(self, pinned); + } + + /// Toggles whether the window is pinned. + pub fn toggle_float_pinned(self) { + self.set_float_pinned(!self.float_pinned()); + } +} diff --git a/src/config.rs b/src/config.rs index 429714cf..c35b90de 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,8 +9,8 @@ use { ifs::wl_seat::SeatId, state::State, utils::{ - clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, unlink_on_drop::UnlinkOnDrop, - xrd::xrd, + clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, + toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, }, }, bincode::Options, @@ -151,6 +151,15 @@ impl ConfigProxy { event, }); } + + pub fn toplevel_removed(&self, id: ToplevelIdentifier) { + let Some(handler) = self.handler.get() else { + return; + }; + if let Some(win) = handler.windows_from_tl_id.remove(&id) { + handler.windows_to_tl_id.remove(&win); + } + } } impl Drop for ConfigProxy { @@ -202,6 +211,9 @@ impl ConfigProxy { timers_by_id: Default::default(), pollable_id: Default::default(), pollables: Default::default(), + window_ids: NumCell::new(1), + windows_from_tl_id: Default::default(), + windows_to_tl_id: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 212c8041..83c1abdf 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -19,7 +19,9 @@ use { theme::{Color, ThemeSized}, tree::{ ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode, - TearingMode, VrrMode, WsMoveConfig, move_ws_to_output, + TearingMode, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig, move_ws_to_output, + toplevel_create_split, toplevel_parent_container, toplevel_set_floating, + toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, @@ -30,6 +32,7 @@ use { oserror::OsError, stack::Stack, timer::{TimerError, TimerFd}, + toplevel_identifier::ToplevelIdentifier, }, }, bincode::Options, @@ -57,6 +60,7 @@ use { TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, + window::Window, xwayland::XScalingMode, }, libloading::Library, @@ -89,6 +93,10 @@ pub(super) struct ConfigProxyHandler { pub pollable_id: NumCell, pub pollables: CopyHashMap>, + + pub window_ids: NumCell, + pub windows_from_tl_id: CopyHashMap, + pub windows_to_tl_id: CopyHashMap, } pub struct Pollable { @@ -315,6 +323,24 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_fullscreen(&self, window: Window) -> Result<(), CphError> { + let tl = self.get_window(window)?; + self.respond(Response::GetWindowFullscreen { + fullscreen: tl.tl_data().is_fullscreen.get(), + }); + Ok(()) + } + + fn handle_set_window_fullscreen( + &self, + window: Window, + fullscreen: bool, + ) -> Result<(), CphError> { + let tl = self.get_window(window)?; + tl.tl_set_fullscreen(fullscreen); + Ok(()) + } + fn handle_set_keymap(&self, seat: Seat, keymap: Keymap) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let keymap = if keymap.is_invalid() { @@ -492,6 +518,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_window_close(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + window.tl_close(); + Ok(()) + } + fn handle_seat_focus(&self, seat: Seat, direction: Direction) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.move_focus(direction.into()); @@ -504,6 +536,14 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.move_child(window, direction.into()); + } + Ok(()) + } + fn handle_get_repeat_rate(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; let (rate, delay) = seat.get_rate(); @@ -530,6 +570,11 @@ impl ConfigProxyHandler { } } + fn get_existing_workspace(&self, ws: Workspace) -> Result>, CphError> { + self.get_workspace(ws) + .map(|ws| self.state.workspaces.get(&*ws)) + } + fn get_device_handler_data( &self, device: InputDevice, @@ -720,8 +765,8 @@ impl ConfigProxyHandler { } fn handle_get_workspace_capture(&self, workspace: Workspace) -> Result<(), CphError> { - let name = self.get_workspace(workspace)?; - let capture = match self.state.workspaces.get(name.as_str()) { + let ws = self.get_existing_workspace(workspace)?; + let capture = match ws { Some(ws) => ws.may_capture.get(), None => self.state.default_workspace_capture.get(), }; @@ -734,8 +779,7 @@ impl ConfigProxyHandler { workspace: Workspace, capture: bool, ) -> Result<(), CphError> { - let name = self.get_workspace(workspace)?; - if let Some(ws) = self.state.workspaces.get(name.as_str()) { + if let Some(ws) = self.get_existing_workspace(workspace)? { ws.may_capture.set(capture); ws.update_has_captures(); } @@ -856,6 +900,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_workspace(&self, window: Window, ws: Workspace) -> Result<(), CphError> { + let window = self.get_window(window)?; + let name = self.get_workspace(ws)?; + let workspace = match self.state.workspaces.get(name.deref()) { + Some(ws) => ws, + _ => match window.node_output() { + Some(o) => o.create_workspace(name.deref()), + _ => return Ok(()), + }, + }; + toplevel_set_workspace(&self.state, window, &workspace); + Ok(()) + } + fn handle_get_device_name(&self, device: InputDevice) -> Result<(), CphError> { let dev = self.get_device_handler_data(device)?; let name = dev.device.name(); @@ -888,13 +946,10 @@ impl ConfigProxyHandler { ) -> Result<(), CphError> { let output = self.get_output_node(connector)?; let ws = match workspace { - WorkspaceSource::Explicit(ws) => { - let name = self.get_workspace(ws)?; - match self.state.workspaces.get(name.as_str()) { - Some(ws) => ws, - _ => return Ok(()), - } - } + WorkspaceSource::Explicit(ws) => match self.get_existing_workspace(ws)? { + Some(ws) => ws, + _ => return Ok(()), + }, WorkspaceSource::Seat(s) => match self.get_seat(s)?.get_output().workspace.get() { Some(ws) => ws, _ => return Ok(()), @@ -1180,6 +1235,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_float_pinned(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowFloatPinned { + pinned: window.tl_pinned(), + }); + Ok(()) + } + + fn handle_set_window_float_pinned(&self, window: Window, pinned: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + window.tl_set_pinned(true, pinned); + Ok(()) + } + fn handle_set_vrr_mode( &self, connector: Option, @@ -1360,6 +1429,24 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_mono(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowMono { + mono: toplevel_parent_container(&*window) + .map(|c| c.mono_child.is_some()) + .unwrap_or(false), + }); + Ok(()) + } + + fn handle_set_window_mono(&self, window: Window, mono: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_mono(mono.then_some(window.as_ref())); + } + Ok(()) + } + fn handle_get_seat_split(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; self.respond(Response::GetSplit { @@ -1377,6 +1464,25 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowSplit { + axis: toplevel_parent_container(&*window) + .map(|c| c.split.get()) + .unwrap_or(ContainerSplit::Horizontal) + .into(), + }); + Ok(()) + } + + fn handle_set_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { + let window = self.get_window(window)?; + if let Some(c) = toplevel_parent_container(&*window) { + c.set_split(axis.into()); + } + Ok(()) + } + fn handle_add_shortcut( &self, seat: Seat, @@ -1480,6 +1586,12 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_create_window_split(&self, window: Window, axis: Axis) -> Result<(), CphError> { + let window = self.get_window(window)?; + toplevel_create_split(&self.state, window, axis.into()); + Ok(()) + } + fn handle_focus_seat_parent(&self, seat: Seat) -> Result<(), CphError> { let seat = self.get_seat(seat)?; seat.focus_parent(); @@ -1509,6 +1621,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowFloating { + floating: window.tl_data().is_floating.get(), + }); + Ok(()) + } + + fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { + let window = self.get_window(window)?; + toplevel_set_floating(&self.state, window, floating); + Ok(()) + } + fn handle_add_pollable(self: &Rc, fd: i32) -> Result<(), CphError> { let fd = match fcntl_dupfd_cloexec(fd, 0) { Ok(fd) => Rc::new(fd), @@ -1577,6 +1703,28 @@ impl ConfigProxyHandler { Ok(()) } + fn tl_to_window(&self, tl: &dyn ToplevelNode) -> Window { + self.tl_id_to_window(tl.tl_data().identifier.get()) + } + + fn tl_id_to_window(&self, tl: ToplevelIdentifier) -> Window { + if let Some(win) = self.windows_from_tl_id.get(&tl) { + return win; + } + let id = Window(self.window_ids.fetch_add(1)); + self.windows_from_tl_id.set(tl, id); + self.windows_to_tl_id.set(id, tl); + id + } + + fn get_window(&self, window: Window) -> Result, CphError> { + self.windows_to_tl_id + .get(&window) + .and_then(|id| self.state.toplevels.get(&id)) + .and_then(|tl| tl.upgrade()) + .ok_or(CphError::WindowDoesNotExist(window)) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -1752,6 +1900,123 @@ impl ConfigProxyHandler { self.state.clients.kill(ClientId::from_raw(client.0)); } + fn handle_get_workspace_window(&self, ws: Workspace) -> Result<(), CphError> { + let window = self + .get_existing_workspace(ws)? + .and_then(|ws| ws.container.get()) + .map(|c| self.tl_to_window(&*c)) + .unwrap_or(Window(0)); + self.respond(Response::GetWorkspaceWindow { window }); + Ok(()) + } + + fn handle_get_seat_keyboard_window(&self, seat: Seat) -> Result<(), CphError> { + let window = self + .get_seat(seat)? + .get_keyboard_node() + .node_toplevel() + .map(|tl| self.tl_to_window(&*tl)) + .unwrap_or(Window(0)); + self.respond(Response::GetSeatKeyboardWindow { window }); + Ok(()) + } + + fn handle_seat_focus_window(&self, seat: Seat, window_id: Window) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + let window = self.get_window(window_id)?; + if !window.node_visible() { + return Err(CphError::WindowNotVisible(window_id)); + } + seat.focus_toplevel(window); + Ok(()) + } + + fn handle_get_window_title(&self, window: Window) -> Result<(), CphError> { + let title = self.get_window(window)?.tl_data().title.borrow().clone(); + self.respond(Response::GetWindowTitle { title }); + Ok(()) + } + + fn handle_get_window_type(&self, window: Window) -> Result<(), CphError> { + let kind = self.get_window(window)?.tl_data().kind.to_window_type(); + self.respond(Response::GetWindowType { kind }); + Ok(()) + } + + fn handle_window_exists(&self, window: Window) { + self.respond(Response::WindowExists { + exists: self.get_window(window).is_ok(), + }); + } + + fn handle_get_window_id(&self, window: Window) -> Result<(), CphError> { + let id = self + .get_window(window)? + .tl_data() + .identifier + .get() + .to_string(); + self.respond(Response::GetWindowId { id: id.to_string() }); + Ok(()) + } + + fn handle_get_window_is_visible(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowIsVisible { + visible: window.node_visible(), + }); + Ok(()) + } + + fn handle_get_window_client(&self, window: Window) -> Result<(), CphError> { + let window = self.get_window(window)?; + self.respond(Response::GetWindowClient { + client: window + .tl_data() + .client + .as_ref() + .map(|c| ConfigClient(c.id.raw())) + .unwrap_or(ConfigClient(0)), + }); + Ok(()) + } + + fn handle_get_window_parent(&self, window: Window) -> Result<(), CphError> { + let window = self + .get_window(window)? + .tl_data() + .parent + .get() + .and_then(|tl| tl.node_into_toplevel()) + .map(|tl| self.tl_to_window(&*tl)) + .unwrap_or(Window(0)); + self.respond(Response::GetWindowParent { window }); + Ok(()) + } + + fn handle_get_window_workspace(&self, window: Window) -> Result<(), CphError> { + let workspace = self + .get_window(window)? + .tl_data() + .workspace + .get() + .map(|ws| self.get_workspace_by_name(&ws.name)) + .unwrap_or(Workspace(0)); + self.respond(Response::GetWindowWorkspace { workspace }); + Ok(()) + } + + fn handle_get_window_children(&self, window: Window) -> Result<(), CphError> { + let mut windows = vec![]; + if let Some(c) = self.get_window(window)?.node_into_container() { + for c in c.children.iter() { + windows.push(self.tl_to_window(&*c.node)); + } + } + self.respond(Response::GetWindowChildren { windows }); + Ok(()) + } + pub fn handle_request(self: &Rc, msg: &[u8]) { if let Err(e) = self.handle_request_(msg) { log::error!("Could not handle client request: {}", ErrorFmt(e)); @@ -2171,6 +2436,82 @@ impl ConfigProxyHandler { .handle_client_is_xwayland(client) .wrn("client_is_xwayland")?, ClientMessage::ClientKill { client } => self.handle_client_kill(client), + ClientMessage::WindowExists { window } => self.handle_window_exists(window), + ClientMessage::GetWorkspaceWindow { workspace } => self + .handle_get_workspace_window(workspace) + .wrn("get_workspace_window")?, + ClientMessage::GetSeatKeyboardWindow { seat } => self + .handle_get_seat_keyboard_window(seat) + .wrn("get_seat_keyboard_window")?, + ClientMessage::SeatFocusWindow { seat, window } => self + .handle_seat_focus_window(seat, window) + .wrn("seat_focus_window")?, + ClientMessage::GetWindowTitle { window } => self + .handle_get_window_title(window) + .wrn("get_window_title")?, + ClientMessage::GetWindowType { window } => { + self.handle_get_window_type(window).wrn("get_window_type")? + } + ClientMessage::GetWindowId { window } => { + self.handle_get_window_id(window).wrn("get_window_id")? + } + ClientMessage::GetWindowParent { window } => self + .handle_get_window_parent(window) + .wrn("get_window_parent")?, + ClientMessage::GetWindowWorkspace { window } => self + .handle_get_window_workspace(window) + .wrn("get_window_workspace")?, + ClientMessage::GetWindowChildren { window } => self + .handle_get_window_children(window) + .wrn("get_window_children")?, + ClientMessage::GetWindowSplit { window } => self + .handle_get_window_split(window) + .wrn("get_window_split")?, + ClientMessage::SetWindowSplit { window, axis } => self + .handle_set_window_split(window, axis) + .wrn("set_window_split")?, + ClientMessage::GetWindowMono { window } => { + self.handle_get_window_mono(window).wrn("get_window_mono")? + } + ClientMessage::SetWindowMono { window, mono } => self + .handle_set_window_mono(window, mono) + .wrn("set_window_mono")?, + ClientMessage::WindowMove { window, direction } => self + .handle_window_move(window, direction) + .wrn("window_move")?, + ClientMessage::CreateWindowSplit { window, axis } => self + .handle_create_window_split(window, axis) + .wrn("create_window_split")?, + ClientMessage::WindowClose { window } => { + self.handle_window_close(window).wrn("close_window")? + } + ClientMessage::GetWindowFloating { window } => self + .handle_get_window_floating(window) + .wrn("get_window_floating")?, + ClientMessage::SetWindowFloating { window, floating } => self + .handle_set_window_floating(window, floating) + .wrn("set_window_floating")?, + ClientMessage::SetWindowWorkspace { window, workspace } => self + .handle_set_window_workspace(window, workspace) + .wrn("set_window_workspace")?, + ClientMessage::SetWindowFullscreen { window, fullscreen } => self + .handle_set_window_fullscreen(window, fullscreen) + .wrn("set_window_fullscreen")?, + ClientMessage::GetWindowFullscreen { window } => self + .handle_get_window_fullscreen(window) + .wrn("get_window_fullscreen")?, + ClientMessage::GetWindowFloatPinned { window } => self + .handle_get_window_float_pinned(window) + .wrn("get_window_float_pinned")?, + ClientMessage::SetWindowFloatPinned { window, pinned } => self + .handle_set_window_float_pinned(window, pinned) + .wrn("set_window_float_pinned")?, + ClientMessage::GetWindowIsVisible { window } => self + .handle_get_window_is_visible(window) + .wrn("get_window_is_visible")?, + ClientMessage::GetWindowClient { window } => self + .handle_get_window_client(window) + .wrn("get_window_client")?, } Ok(()) } @@ -2248,6 +2589,10 @@ enum CphError { UnknownTransferFunction(ConfigTransferFunction), #[error("Client {0:?} does not exist")] ClientDoesNotExist(ConfigClient), + #[error("Window {0:?} does not exist")] + WindowDoesNotExist(Window), + #[error("Window {0:?} is not visible")] + WindowNotVisible(Window), } trait WithRequestName { diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b37978ba..f9fffa7a 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -79,7 +79,8 @@ use { state::{DeviceHandlerData, State}, tree::{ ContainerNode, ContainerSplit, Direction, FoundNode, Node, OutputNode, ToplevelNode, - WorkspaceNode, generic_node_visitor, + WorkspaceNode, generic_node_visitor, toplevel_create_split, toplevel_parent_container, + toplevel_set_floating, toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, bindings::PerClientBindings, clonecell::CloneCell, @@ -401,6 +402,10 @@ impl WlSeatGlobal { self.cursor_user_group.latest_output() } + pub fn get_keyboard_node(&self) -> Rc { + self.keyboard_node.get() + } + pub fn get_keyboard_output(&self) -> Option> { self.keyboard_node.get().node_output() } @@ -410,38 +415,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - if tl.tl_data().is_fullscreen.get() { - return; - } - let old_ws = match tl.tl_data().workspace.get() { - Some(ws) => ws, - _ => return, - }; - if old_ws.id == ws.id { - return; - } - let cn = match tl.tl_data().parent.get() { - Some(cn) => cn, - _ => return, - }; - let kb_foci = collect_kb_foci(tl.clone()); - cn.cnode_remove_child2(&*tl, true); - if !ws.visible.get() { - for focus in kb_foci { - old_ws.clone().node_do_focus(&focus, Direction::Unspecified); - } - } - if tl.tl_data().is_floating.get() { - self.state.map_floating( - tl.clone(), - tl.tl_data().float_width.get(), - tl.tl_data().float_height.get(), - ws, - None, - ); - } else { - self.state.map_tiled_on(tl, ws); - } + toplevel_set_workspace(&self.state, tl, ws); } pub fn mark_last_active(self: &Rc) { @@ -556,11 +530,7 @@ impl WlSeatGlobal { pub fn kb_parent_container(&self) -> Option> { if let Some(tl) = self.keyboard_node.get().node_toplevel() { - if let Some(parent) = tl.tl_data().parent.get() { - if let Some(container) = parent.node_into_container() { - return Some(container); - } - } + return toplevel_parent_container(&*tl); } None } @@ -595,21 +565,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - if tl.tl_data().is_fullscreen.get() { - return; - } - let ws = match tl.tl_data().workspace.get() { - Some(ws) => ws, - _ => return, - }; - let pn = match tl.tl_data().parent.get() { - Some(pn) => pn, - _ => return, - }; - if let Some(pn) = pn.node_into_containing_node() { - let cn = ContainerNode::new(&self.state, &ws, tl.clone(), axis); - pn.cnode_replace_child(&*tl, cn); - } + toplevel_create_split(&self.state, tl, axis); } pub fn focus_parent(self: &Rc) { @@ -634,29 +590,7 @@ impl WlSeatGlobal { Some(tl) => tl, _ => return, }; - self.set_tl_floating(tl, floating); - } - - pub fn set_tl_floating(self: &Rc, tl: Rc, floating: bool) { - let data = tl.tl_data(); - if data.is_fullscreen.get() { - return; - } - if data.is_floating.get() == floating { - return; - } - let parent = match data.parent.get() { - Some(p) => p, - _ => return, - }; - if !floating { - parent.cnode_remove_child2(&*tl, true); - self.state.map_tiled(tl); - } else if let Some(ws) = data.workspace.get() { - parent.cnode_remove_child2(&*tl, true); - let (width, height) = data.float_size(&ws); - self.state.map_floating(tl, width, height, &ws, None); - } + toplevel_set_floating(&self.state, tl, floating); } pub fn get_rate(&self) -> (i32, i32) { diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 9c8178ad..1c6664c2 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -13,7 +13,7 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, StackedNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, WorkspaceNode, default_tile_drag_destination, + ToplevelNodeBase, ToplevelType, WorkspaceNode, default_tile_drag_destination, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, wire::WlSurfaceId, @@ -205,16 +205,19 @@ impl Xwindow { if xsurface.xwindow.is_some() { return Err(XWindowError::AlreadyAttached); } + let id = data.state.node_ids.next(); let slf = Rc::new_cyclic(|weak| { let tld = ToplevelData::new( &data.state, data.info.title.borrow_mut().clone().unwrap_or_default(), Some(surface.client.clone()), + ToplevelType::XWindow, + id, weak, ); tld.pos.set(surface.extents.get()); Self { - id: data.state.node_ids.next(), + id, data: data.clone(), display_link: Default::default(), toplevel_data: tld, diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index cca80ab2..0931ff8d 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -27,7 +27,8 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, ToplevelNodeId, WorkspaceNode, default_tile_drag_destination, + ToplevelNodeBase, ToplevelNodeId, ToplevelType, WorkspaceNode, + default_tile_drag_destination, }, utils::{clonecell::CloneCell, hash_map_ext::HashMapExt}, wire::{XdgToplevelId, xdg_toplevel::*}, @@ -133,11 +134,12 @@ impl XdgToplevel { states.insert(STATE_CONSTRAINED_BOTTOM); } let state = &surface.surface.client.state; + let node_id = state.node_ids.next(); Self { id, state: state.clone(), xdg: surface.clone(), - node_id: state.node_ids.next(), + node_id, parent: Default::default(), children: RefCell::new(Default::default()), states: RefCell::new(states), @@ -152,6 +154,8 @@ impl XdgToplevel { state, String::new(), Some(surface.surface.client.clone()), + ToplevelType::XdgToplevel, + node_id, slf, ), drag: Default::default(), diff --git a/src/state.rs b/src/state.rs index ff0a278d..3c96c63b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -951,6 +951,13 @@ impl State { self.workspace_managers.clear(); } + pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { + self.toplevels.remove(&id); + if let Some(config) = self.config.get() { + config.toplevel_removed(id); + } + } + pub fn damage_hardware_cursors(&self, render: bool) { for output in self.root.outputs.lock().values() { if let Some(hc) = output.hardware_cursor.get() { diff --git a/src/tree/container.rs b/src/tree/container.rs index 7ede6614..fd56767f 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -19,7 +19,8 @@ use { tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, OutputNode, TddType, TileDragDestination, ToplevelData, ToplevelNode, ToplevelNodeBase, - WorkspaceNode, default_tile_drag_bounds, walker::NodeVisitor, + ToplevelType, WorkspaceNode, default_tile_drag_bounds, toplevel_set_floating, + walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, @@ -212,8 +213,9 @@ impl ContainerNode { let child_node_ref = child_node.clone(); let mut child_nodes = AHashMap::new(); child_nodes.insert(child.node_id(), child_node); + let id = state.node_ids.next(); let slf = Rc::new_cyclic(|weak| Self { - id: state.node_ids.next(), + id, split: Cell::new(split), mono_child: CloneCell::new(None), mono_body: Cell::new(Default::default()), @@ -237,7 +239,14 @@ impl ContainerNode { state: state.clone(), render_data: Default::default(), scroller: Default::default(), - toplevel_data: ToplevelData::new(state, Default::default(), None, weak), + toplevel_data: ToplevelData::new( + state, + Default::default(), + None, + ToplevelType::Container, + id, + weak, + ), attention_requests: Default::default(), }); child.tl_set_parent(slf.clone()); @@ -1239,7 +1248,7 @@ impl ContainerNode { && kind == SeatOpKind::Move { drop(seat_datas); - seat.set_tl_floating(child.node.clone(), true); + toplevel_set_floating(&self.state, child.node.clone(), true); return; } seat_data.op = Some(SeatOp { diff --git a/src/tree/float.rs b/src/tree/float.rs index e75335ec..f030a13e 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -16,7 +16,7 @@ use { tree::{ ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, OutputNode, PinnedNode, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, - walker::NodeVisitor, + toplevel_set_floating, walker::NodeVisitor, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -603,7 +603,7 @@ impl FloatNode { { if let Some(tl) = self.child.get() { drop(cursors); - seat.set_tl_floating(tl, false); + toplevel_set_floating(&self.state, tl, false); return; } } diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index f28eee0b..1dd3ee3b 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -12,7 +12,7 @@ use { tree::{ ContainerSplit, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, TileDragDestination, ToplevelData, ToplevelNode, - ToplevelNodeBase, default_tile_drag_destination, + ToplevelNodeBase, ToplevelType, default_tile_drag_destination, }, utils::{ asyncevent::AsyncEvent, errorfmt::ErrorFmt, on_drop_event::OnDropEvent, @@ -49,12 +49,15 @@ pub async fn placeholder_render_textures(state: Rc) { impl PlaceholderNode { pub fn new_for(state: &Rc, node: Rc, slf: &Weak) -> Self { + let id = state.node_ids.next(); Self { - id: state.node_ids.next(), + id, toplevel: ToplevelData::new( state, node.tl_data().title.borrow().clone(), node.node_client(), + ToplevelType::Placeholder, + id, slf, ), destroyed: Default::default(), @@ -65,9 +68,17 @@ impl PlaceholderNode { } pub fn new_empty(state: &Rc, slf: &Weak) -> Self { + let id = state.node_ids.next(); Self { - id: state.node_ids.next(), - toplevel: ToplevelData::new(state, String::new(), None, slf), + id, + toplevel: ToplevelData::new( + state, + String::new(), + None, + ToplevelType::Placeholder, + id, + slf, + ), destroyed: Default::default(), update_textures_scheduled: Default::default(), state: state.clone(), diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 45aed49e..dc8c1857 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -30,6 +30,7 @@ use { JayToplevelId, }, }, + jay_config::{window, window::WindowType}, std::{ cell::{Cell, RefCell}, ops::Deref, @@ -254,7 +255,29 @@ impl ToplevelOpt { } } +#[derive(Debug)] +pub enum ToplevelType { + Container, + Placeholder, + XdgToplevel, + XWindow, +} + +impl ToplevelType { + pub fn to_window_type(&self) -> WindowType { + match self { + ToplevelType::Container => window::CONTAINER, + ToplevelType::Placeholder => window::PLACEHOLDER, + ToplevelType::XdgToplevel => window::XDG_TOPLEVEL, + ToplevelType::XWindow => window::X_WINDOW, + } + } +} + pub struct ToplevelData { + #[expect(dead_code)] + pub node_id: NodeId, + pub kind: ToplevelType, pub self_active: Cell, pub client: Option>, pub state: Rc, @@ -291,11 +314,16 @@ impl ToplevelData { state: &Rc, title: String, client: Option>, + kind: ToplevelType, + node_id: impl Into, slf: &Weak, ) -> Self { + let node_id = node_id.into(); let id = toplevel_identifier(); state.toplevels.set(id, slf.clone()); Self { + node_id, + kind, self_active: Cell::new(false), client, state: state.clone(), @@ -372,7 +400,7 @@ impl ToplevelData { { let id = toplevel_identifier(); let prev = self.identifier.replace(id); - self.state.toplevels.remove(&prev); + self.state.remove_toplevel_id(prev); self.state.toplevels.set(id, self.slf.clone()); } { @@ -620,7 +648,7 @@ impl ToplevelData { impl Drop for ToplevelData { fn drop(&mut self) { - self.state.toplevels.remove(&self.identifier.get()); + self.state.remove_toplevel_id(self.identifier.get()); } } @@ -662,3 +690,82 @@ pub fn default_tile_drag_bounds(t: &T, split: Cont ContainerSplit::Vertical => t.node_absolute_position().height() / FACTOR, } } + +pub fn toplevel_parent_container(tl: &dyn ToplevelNode) -> Option> { + if let Some(parent) = tl.tl_data().parent.get() { + if let Some(container) = parent.node_into_container() { + return Some(container); + } + } + None +} + +pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: ContainerSplit) { + if tl.tl_data().is_fullscreen.get() { + return; + } + let ws = match tl.tl_data().workspace.get() { + Some(ws) => ws, + _ => return, + }; + let pn = match tl.tl_data().parent.get() { + Some(pn) => pn, + _ => return, + }; + if let Some(pn) = pn.node_into_containing_node() { + let cn = ContainerNode::new(state, &ws, tl.clone(), axis); + pn.cnode_replace_child(&*tl, cn); + } +} + +pub fn toplevel_set_floating(state: &Rc, tl: Rc, floating: bool) { + let data = tl.tl_data(); + if data.is_fullscreen.get() { + return; + } + if data.is_floating.get() == floating { + return; + } + let parent = match data.parent.get() { + Some(p) => p, + _ => return, + }; + if !floating { + parent.cnode_remove_child2(&*tl, true); + state.map_tiled(tl); + } else if let Some(ws) = data.workspace.get() { + parent.cnode_remove_child2(&*tl, true); + let (width, height) = data.float_size(&ws); + state.map_floating(tl, width, height, &ws, None); + } +} + +pub fn toplevel_set_workspace(state: &Rc, tl: Rc, ws: &Rc) { + if tl.tl_data().is_fullscreen.get() { + return; + } + let old_ws = match tl.tl_data().workspace.get() { + Some(ws) => ws, + _ => return, + }; + if old_ws.id == ws.id { + return; + } + let cn = match tl.tl_data().parent.get() { + Some(cn) => cn, + _ => return, + }; + let kb_foci = collect_kb_foci(tl.clone()); + cn.cnode_remove_child2(&*tl, true); + if !ws.visible.get() { + for focus in kb_foci { + old_ws.clone().node_do_focus(&focus, Direction::Unspecified); + } + } + if tl.tl_data().is_floating.get() { + let (width, height) = tl.tl_data().float_size(ws); + state.map_floating(tl.clone(), width, height, ws, None); + } else { + state.map_tiled_on(tl, ws); + } +} diff --git a/src/utils/clonecell.rs b/src/utils/clonecell.rs index 1364aa81..aebb5c72 100644 --- a/src/utils/clonecell.rs +++ b/src/utils/clonecell.rs @@ -1,9 +1,12 @@ use { - crate::utils::{ - linkedlist::NodeRef, - ptr_ext::{MutPtrExt, PtrExt}, + crate::{ + tree::NodeId, + utils::{ + linkedlist::NodeRef, + ptr_ext::{MutPtrExt, PtrExt}, + }, }, - jay_config::keyboard::mods::Modifiers, + jay_config::{keyboard::mods::Modifiers, window::Window}, std::{ cell::UnsafeCell, fmt::{Debug, Formatter}, @@ -97,3 +100,7 @@ unsafe impl UnsafeCellCloneSafe for usize {} unsafe impl UnsafeCellCloneSafe for (A, B) {} unsafe impl UnsafeCellCloneSafe for Modifiers {} + +unsafe impl UnsafeCellCloneSafe for NodeId {} + +unsafe impl UnsafeCellCloneSafe for Window {} diff --git a/src/utils/toplevel_identifier.rs b/src/utils/toplevel_identifier.rs index 0825e299..09d894ee 100644 --- a/src/utils/toplevel_identifier.rs +++ b/src/utils/toplevel_identifier.rs @@ -1,5 +1,8 @@ use { - crate::utils::opaque::{OPAQUE_LEN, Opaque, OpaqueError, opaque}, + crate::utils::{ + clonecell::UnsafeCellCloneSafe, + opaque::{OPAQUE_LEN, Opaque, OpaqueError, opaque}, + }, arrayvec::ArrayString, std::{ fmt::{Display, Formatter}, @@ -10,6 +13,8 @@ use { #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct ToplevelIdentifier(Opaque); +unsafe impl UnsafeCellCloneSafe for ToplevelIdentifier {} + pub fn toplevel_identifier() -> ToplevelIdentifier { ToplevelIdentifier(opaque()) } From 597636fba6a1c99ab72ec3b274385c1fcb50ba15 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 12:31:54 +0200 Subject: [PATCH 08/35] io_uring: add debounce future --- src/io_uring.rs | 41 ++++++++++++++++++++++++++++++++++------ src/io_uring/debounce.rs | 33 ++++++++++++++++++++++++++++++++ src/utils/syncqueue.rs | 1 - 3 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/io_uring/debounce.rs diff --git a/src/io_uring.rs b/src/io_uring.rs index 7ed3d63e..d5e1a249 100644 --- a/src/io_uring.rs +++ b/src/io_uring.rs @@ -7,6 +7,7 @@ use { crate::{ async_engine::AsyncEngine, io_uring::{ + debounce::Debouncer, ops::{ accept::AcceptTask, async_cancel::AsyncCancelTask, connect::ConnectTask, poll::PollTask, poll_external::PollExternalTask, read_write::ReadWriteTask, @@ -29,6 +30,7 @@ use { copyhashmap::CopyHashMap, errorfmt::ErrorFmt, mmap::{Mmapped, mmap}, + numcell::NumCell, oserror::OsError, ptr_ext::{MutPtrExt, PtrExt}, stack::Stack, @@ -42,6 +44,7 @@ use { AtomicU32, Ordering::{Acquire, Relaxed, Release}, }, + task::Waker, }, thiserror::Error, uapi::{ @@ -61,6 +64,7 @@ macro_rules! map_err { }}; } +mod debounce; mod ops; mod pending_result; mod sys; @@ -242,6 +246,8 @@ impl IoUring { cached_connects: Default::default(), cached_accepts: Default::default(), fd_ids_scratch: Default::default(), + iteration: Default::default(), + yields: Default::default(), }); Ok(Rc::new(Self { ring: data })) } @@ -259,6 +265,16 @@ impl IoUring { pub fn cancel(&self, id: IoUringTaskId) { self.ring.cancel_task(id); } + + #[expect(dead_code)] + pub fn debouncer(&self, max: u64) -> Debouncer { + Debouncer { + cur: Default::default(), + max, + iteration: Cell::new(self.ring.iteration.get()), + ring: self.ring.clone(), + } + } } struct IoUringData { @@ -306,6 +322,9 @@ struct IoUringData { cached_accepts: Stack>, fd_ids_scratch: RefCell>, + + iteration: NumCell, + yields: SyncQueue, } unsafe trait Task { @@ -326,6 +345,10 @@ impl IoUringData { fn run(&self) -> Result<(), IoUringError> { let mut to_submit = 0; loop { + self.iteration.fetch_add(1); + while let Some(ev) = self.yields.pop() { + ev.wake(); + } loop { self.eng.dispatch(); if self.destroyed.get() { @@ -336,12 +359,18 @@ impl IoUringData { } } to_submit += self.encode(); - let res = if to_submit == 0 { - io_uring_enter(self.fd.raw(), 0, 1, IORING_ENTER_GETEVENTS) - } else if self.to_encode.is_empty() { - io_uring_enter(self.fd.raw(), to_submit as _, 1, IORING_ENTER_GETEVENTS) - } else { - io_uring_enter(self.fd.raw(), !0, 0, 0) + let res = { + let (to_submit, mut min_complete, flags) = if to_submit == 0 { + (0, 1, IORING_ENTER_GETEVENTS) + } else if self.to_encode.is_empty() { + (to_submit as _, 1, IORING_ENTER_GETEVENTS) + } else { + (!0, 0, 0) + }; + if self.yields.is_not_empty() { + min_complete = 0; + } + io_uring_enter(self.fd.raw(), to_submit, min_complete, flags) }; let mut submitted_any = false; match res { diff --git a/src/io_uring/debounce.rs b/src/io_uring/debounce.rs new file mode 100644 index 00000000..6c68693f --- /dev/null +++ b/src/io_uring/debounce.rs @@ -0,0 +1,33 @@ +use { + crate::{io_uring::IoUringData, utils::numcell::NumCell}, + std::{cell::Cell, future::poll_fn, rc::Rc, task::Poll}, +}; + +pub struct Debouncer { + pub(super) cur: NumCell, + pub(super) max: u64, + pub(super) iteration: Cell, + pub(super) ring: Rc, +} + +impl Debouncer { + #[expect(dead_code)] + pub async fn debounce(&self) { + let iteration = self.ring.iteration.get(); + if self.iteration.replace(iteration) != iteration { + self.cur.set(0); + } + if self.cur.fetch_add(1) > self.max { + poll_fn(|ctx| { + if self.ring.iteration.get() > iteration { + Poll::Ready(()) + } else { + self.ring.yields.push(ctx.waker().clone()); + Poll::Pending + } + }) + .await; + self.cur.set(0); + } + } +} diff --git a/src/utils/syncqueue.rs b/src/utils/syncqueue.rs index 1a0852e1..d8bc938f 100644 --- a/src/utils/syncqueue.rs +++ b/src/utils/syncqueue.rs @@ -44,7 +44,6 @@ impl SyncQueue { unsafe { self.el.get().deref_mut().is_empty() } } - #[expect(dead_code)] pub fn is_not_empty(&self) -> bool { !self.is_empty() } From 17e715cde4a17d0af8189f7d669854dcc69a3da3 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 2 May 2025 16:52:04 +0200 Subject: [PATCH 09/35] criteria: add infrastructure --- Cargo.lock | 1 + Cargo.toml | 1 + src/criteria.rs | 96 ++++++++++ src/criteria/crit_graph.rs | 16 ++ src/criteria/crit_graph/crit_downstream.rs | 52 ++++++ src/criteria/crit_graph/crit_middle.rs | 111 +++++++++++ src/criteria/crit_graph/crit_root.rs | 173 +++++++++++++++++ src/criteria/crit_graph/crit_target.rs | 48 +++++ src/criteria/crit_graph/crit_upstream.rs | 176 ++++++++++++++++++ src/criteria/crit_leaf.rs | 156 ++++++++++++++++ src/criteria/crit_matchers.rs | 4 + .../crit_matchers/critm_any_or_all.rs | 73 ++++++++ src/criteria/crit_matchers/critm_constant.rs | 59 ++++++ src/criteria/crit_matchers/critm_exactly.rs | 61 ++++++ src/criteria/crit_matchers/critm_string.rs | 46 +++++ src/criteria/crit_per_target_data.rs | 136 ++++++++++++++ src/macros.rs | 7 +- src/main.rs | 1 + 18 files changed, 1214 insertions(+), 3 deletions(-) create mode 100644 src/criteria.rs create mode 100644 src/criteria/crit_graph.rs create mode 100644 src/criteria/crit_graph/crit_downstream.rs create mode 100644 src/criteria/crit_graph/crit_middle.rs create mode 100644 src/criteria/crit_graph/crit_root.rs create mode 100644 src/criteria/crit_graph/crit_target.rs create mode 100644 src/criteria/crit_graph/crit_upstream.rs create mode 100644 src/criteria/crit_leaf.rs create mode 100644 src/criteria/crit_matchers.rs create mode 100644 src/criteria/crit_matchers/critm_any_or_all.rs create mode 100644 src/criteria/crit_matchers/critm_constant.rs create mode 100644 src/criteria/crit_matchers/critm_exactly.rs create mode 100644 src/criteria/crit_matchers/critm_string.rs create mode 100644 src/criteria/crit_per_target_data.rs diff --git a/Cargo.lock b/Cargo.lock index da6f0291..bc8963a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -602,6 +602,7 @@ dependencies = [ "pin-project", "png", "rand", + "regex", "repc", "rustc-demangle", "serde", diff --git a/Cargo.toml b/Cargo.toml index dd43f8d5..36752338 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ rustc-demangle = { version = "0.1.24", optional = true } tracy-client-sys = { version = "0.24.1", features = ["ondemand", "manual-lifetime", "debuginfod", "demangle"], optional = true } kbvm = "0.1.4" tiny-skia = { version = "0.11.4", default-features = false, features = ["std"] } +regex = "1.11.1" [build-dependencies] repc = "0.1.1" diff --git a/src/criteria.rs b/src/criteria.rs new file mode 100644 index 00000000..5e6f21f2 --- /dev/null +++ b/src/criteria.rs @@ -0,0 +1,96 @@ +mod crit_graph; +pub mod crit_leaf; +mod crit_matchers; +mod crit_per_target_data; + +use { + crate::{ + criteria::{ + crit_graph::{CritMgr, CritMiddle, CritRoot, CritRootCriterion, CritRootFixed}, + crit_leaf::CritLeafMatcher, + crit_matchers::{critm_any_or_all::CritMatchAnyOrAll, critm_exactly::CritMatchExactly}, + }, + utils::copyhashmap::CopyHashMap, + }, + linearize::StaticMap, + regex::Regex, + std::rc::{Rc, Weak}, +}; +pub use { + crit_graph::{CritTarget, CritUpstreamNode}, + crit_per_target_data::CritDestroyListener, +}; + +linear_ids!(CritMatcherIds, CritMatcherId, u64); + +type RootMatcherMap = CopyHashMap>>; +type FixedRootMatcher = StaticMap>>>; + +#[derive(Clone)] +#[expect(dead_code)] +pub enum CritLiteralOrRegex { + Literal(String), + Regex(Regex), +} + +impl CritLiteralOrRegex { + fn matches(&self, string: &str) -> bool { + match self { + CritLiteralOrRegex::Literal(p) => string == p, + CritLiteralOrRegex::Regex(r) => r.is_match(string), + } + } +} + +#[expect(dead_code)] +pub trait CritMgrExt: CritMgr { + fn list( + &self, + upstream: &[Rc>], + all: bool, + ) -> Rc> { + if upstream.is_empty() { + return self.match_constant()[all].clone(); + } + CritMiddle::new(self, upstream, CritMatchAnyOrAll::new(upstream, all)) + } + + fn exactly( + &self, + upstream: &[Rc>], + num: usize, + ) -> Rc> { + if num > upstream.len() { + return self.match_constant()[false].clone(); + } + if num == 0 { + let upstream: Vec<_> = upstream.iter().map(|u| u.not(self)).collect(); + return self.list(&upstream, true); + } + CritMiddle::new(self, upstream, CritMatchExactly::new(upstream, num)) + } + + fn leaf( + &self, + upstream: &Rc>, + on_match: impl Fn(::LeafData) -> Box + 'static, + ) -> Rc> { + CritLeafMatcher::new(self, upstream, on_match) + } + + fn not( + &self, + upstream: &Rc>, + ) -> Rc> { + upstream.not(self) + } + + fn root(&self, criterion: T) -> Rc> + where + T: CritRootCriterion, + { + CritRoot::new(self.roots(), self.id(), criterion) + } +} + +impl CritMgrExt for T where T: CritMgr {} diff --git a/src/criteria/crit_graph.rs b/src/criteria/crit_graph.rs new file mode 100644 index 00000000..e7ed8be4 --- /dev/null +++ b/src/criteria/crit_graph.rs @@ -0,0 +1,16 @@ +mod crit_downstream; +mod crit_middle; +mod crit_root; +mod crit_target; +mod crit_upstream; + +pub use { + crit_downstream::{CritDownstream, CritDownstreamData}, + crit_middle::{CritMiddle, CritMiddleCriterion}, + crit_root::{ + CritFixedRootCriterion, CritFixedRootCriterionBase, CritRoot, CritRootCriterion, + CritRootFixed, + }, + crit_target::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, + crit_upstream::{CritUpstreamData, CritUpstreamNode}, +}; diff --git a/src/criteria/crit_graph/crit_downstream.rs b/src/criteria/crit_graph/crit_downstream.rs new file mode 100644 index 00000000..cf3f50be --- /dev/null +++ b/src/criteria/crit_graph/crit_downstream.rs @@ -0,0 +1,52 @@ +use { + crate::criteria::{ + CritMatcherId, + crit_graph::{CritTarget, crit_upstream::CritUpstreamNode}, + }, + std::rc::Rc, +}; + +pub struct CritDownstreamData +where + Target: CritTarget, +{ + id: CritMatcherId, + pub(super) upstream: Vec>>, +} + +pub trait CritDownstream: 'static { + fn update_matched(self: Rc, target: &Target, matched: bool); +} + +impl CritDownstreamData +where + Target: CritTarget, +{ + pub fn new(id: CritMatcherId, upstream: &[Rc>]) -> Self { + Self { + id, + upstream: upstream.to_vec(), + } + } + + pub fn attach(&self, slf: &Rc>) { + for upstream in &self.upstream { + upstream.attach(self.id, slf.clone() as _); + } + } + + pub fn not(&self, mgr: &Target::Mgr) -> Vec>> { + self.upstream.iter().map(|n| n.not(mgr)).collect() + } +} + +impl Drop for CritDownstreamData +where + Target: CritTarget, +{ + fn drop(&mut self) { + for el in &self.upstream { + el.detach(self.id); + } + } +} diff --git a/src/criteria/crit_graph/crit_middle.rs b/src/criteria/crit_graph/crit_middle.rs new file mode 100644 index 00000000..f8e76041 --- /dev/null +++ b/src/criteria/crit_graph/crit_middle.rs @@ -0,0 +1,111 @@ +use { + crate::criteria::{ + CritUpstreamNode, + crit_graph::{ + CritDownstream, CritDownstreamData, CritTarget, CritUpstreamData, + crit_target::CritMgr, + crit_upstream::{CritUpstreamNodeBase, CritUpstreamNodeData}, + }, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, + }, + std::rc::Rc, +}; + +pub struct CritMiddle +where + Target: CritTarget, + T: CritMiddleCriterion, +{ + upstream: CritDownstreamData, + downstream: CritUpstreamData>, + criterion: T, +} + +#[derive(Default)] +pub struct CritMiddleData { + matches: usize, + data: T, +} + +pub trait CritMiddleCriterion: Sized + 'static { + type Data: Default; + type Not: CritMiddleCriterion; + + fn update_matched(&self, target: &Target, node: &mut Self::Data, matched: bool) -> bool; + fn pull(&self, upstream: &[Rc>], target: &Target) -> bool; + fn not(&self) -> Self::Not; +} + +impl CritMiddle +where + Target: CritTarget, + T: CritMiddleCriterion, +{ + pub fn new( + mgr: &Target::Mgr, + upstream: &[Rc>], + criterion: T, + ) -> Rc { + let id = mgr.id(); + let slf = Rc::new_cyclic(|slf| Self { + upstream: CritDownstreamData::new(id, upstream), + downstream: CritUpstreamData::new(slf, id), + criterion, + }); + slf.upstream.attach(&slf); + slf + } +} + +impl CritDownstream for CritMiddle +where + Target: CritTarget, + T: CritMiddleCriterion, +{ + fn update_matched(self: Rc, target: &Target, matched: bool) { + let mut node = self.downstream.get_or_create(target); + let change = self + .criterion + .update_matched(target, &mut node.data, matched); + let matches = match matched { + true => node.matches + 1, + false => node.matches - 1, + }; + node.matches = matches; + self.downstream + .update_matched(target, node, change, matches == 0); + } +} + +impl CritUpstreamNodeBase for CritMiddle +where + Target: CritTarget, + T: CritMiddleCriterion, +{ + type Data = CritMiddleData; + + fn data(&self) -> &CritUpstreamData { + &self.downstream + } + + fn not(&self, mgr: &Target::Mgr) -> Rc> { + let upstream = self.upstream.not(mgr); + CritMiddle::new(mgr, &upstream, self.criterion.not()) + } + + fn pull(&self, target: &Target) -> bool { + self.criterion.pull(&self.upstream.upstream, target) + } +} + +impl CritDestroyListenerBase for CritMiddle +where + Target: CritTarget, + T: CritMiddleCriterion, +{ + type Data = CritUpstreamNodeData>; + + fn data(&self) -> &CritPerTargetData { + &self.downstream.nodes + } +} diff --git a/src/criteria/crit_graph/crit_root.rs b/src/criteria/crit_graph/crit_root.rs new file mode 100644 index 00000000..20828765 --- /dev/null +++ b/src/criteria/crit_graph/crit_root.rs @@ -0,0 +1,173 @@ +use { + crate::criteria::{ + CritMatcherId, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, + crit_graph::{ + CritTarget, CritUpstreamData, + crit_target::CritMgr, + crit_upstream::{CritUpstreamNodeBase, CritUpstreamNodeData}, + }, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, + }, + std::{marker::PhantomData, rc::Rc}, +}; + +pub struct CritRoot +where + Target: CritTarget, + T: CritRootCriterion, +{ + id: CritMatcherId, + downstream: CritUpstreamData, + not: bool, + criterion: Rc, + roots: Rc, +} + +pub struct CritRootFixed(pub Crit, pub PhantomData); + +pub trait CritRootCriterion: Sized + 'static +where + Target: CritTarget, +{ + fn matches(&self, data: &Target) -> bool; + fn nodes(roots: &Target::RootMatchers) -> Option<&RootMatcherMap> { + let _ = roots; + None + } + fn not(&self, mgr: &Target::Mgr) -> Option>> { + let _ = mgr; + None + } +} + +pub trait CritFixedRootCriterionBase: Sized + 'static +where + Target: CritTarget, +{ + fn constant(&self) -> bool; + fn not<'a>(&self, mgr: &'a Target::Mgr) -> &'a FixedRootMatcher + where + Self: CritFixedRootCriterion; +} + +pub trait CritFixedRootCriterion: CritFixedRootCriterionBase +where + Target: CritTarget, +{ + const COMPARE: bool = true; + + fn matches(&self, data: &Target) -> bool; +} + +impl CritRootCriterion for CritRootFixed +where + Target: CritTarget, + T: CritFixedRootCriterion, +{ + fn matches(&self, data: &Target) -> bool { + let mut res = self.0.matches(data); + if T::COMPARE { + res = res == self.0.constant(); + } + res + } + + fn not(&self, mgr: &Target::Mgr) -> Option>> { + Some(self.0.not(mgr)[!self.0.constant()].clone()) + } +} + +impl CritUpstreamNodeBase for CritRoot +where + Target: CritTarget, + T: CritRootCriterion, +{ + type Data = (); + + fn data(&self) -> &CritUpstreamData { + &self.downstream + } + + fn not(&self, mgr: &Target::Mgr) -> Rc> { + if let Some(node) = self.criterion.not(mgr) { + return node; + } + let id = mgr.id(); + Self::new_(&self.roots, id, self.criterion.clone(), !self.not) + } + + fn pull(&self, target: &Target) -> bool { + self.criterion.matches(target) ^ self.not + } +} + +impl CritRoot +where + Target: CritTarget, + T: CritRootCriterion, +{ + pub fn new(roots: &Rc, id: CritMatcherId, criterion: T) -> Rc { + Self::new_(roots, id, Rc::new(criterion), false) + } + + fn new_( + roots: &Rc, + id: CritMatcherId, + criterion: Rc, + not: bool, + ) -> Rc { + let slf = Rc::new_cyclic(|slf| Self { + id, + downstream: CritUpstreamData::new(slf, id), + not, + criterion, + roots: roots.clone(), + }); + if let Some(nodes) = T::nodes(roots) { + nodes.set(id, Rc::downgrade(&slf)); + } + slf + } + + #[expect(dead_code)] + pub fn handle(&self, target: &Target) { + let new = self.criterion.matches(target) ^ self.not; + let node = match new { + true => self.downstream.get_or_create(target), + false => match self.downstream.get(target) { + Some(n) => n, + None => return, + }, + }; + self.downstream.update_matched(target, node, new, !new); + } + + #[expect(dead_code)] + pub fn has_downstream(&self) -> bool { + self.downstream.has_downstream() + } +} + +impl CritDestroyListenerBase for CritRoot +where + Target: CritTarget, + T: CritRootCriterion, +{ + type Data = CritUpstreamNodeData; + + fn data(&self) -> &CritPerTargetData { + &self.downstream.nodes + } +} + +impl Drop for CritRoot +where + Target: CritTarget, + T: CritRootCriterion, +{ + fn drop(&mut self) { + if let Some(nodes) = T::nodes(&self.roots) { + nodes.remove(&self.id); + } + } +} diff --git a/src/criteria/crit_graph/crit_target.rs b/src/criteria/crit_graph/crit_target.rs new file mode 100644 index 00000000..18974748 --- /dev/null +++ b/src/criteria/crit_graph/crit_target.rs @@ -0,0 +1,48 @@ +use { + crate::{ + criteria::{ + CritDestroyListener, CritMatcherId, FixedRootMatcher, crit_leaf::CritLeafEvent, + crit_matchers::critm_constant::CritMatchConstant, + }, + utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, + }, + std::{ + hash::Hash, + rc::{Rc, Weak}, + }, +}; + +pub trait CritMgr: 'static { + type Target: CritTarget; + + fn id(&self) -> CritMatcherId; + fn leaf_events(&self) -> &Rc>>; + fn match_constant(&self) -> &FixedRootMatcher>; + fn roots(&self) -> &Rc<::RootMatchers>; +} + +pub trait CritTarget: 'static { + type Id: Copy + Hash + Eq; + type Mgr: CritMgr; + type RootMatchers; + type LeafData: Copy + Eq; + type Owner: WeakCritTargetOwner; + + fn owner(&self) -> Self::Owner; + fn id(&self) -> Self::Id; + fn destroyed(&self) -> &CopyHashMap>>; + fn leaf_data(&self) -> Self::LeafData; +} + +pub trait CritTargetOwner: 'static { + type Target: CritTarget; + + fn data(&self) -> &Self::Target; +} + +pub trait WeakCritTargetOwner: 'static { + type Target: CritTarget; + type Owner: CritTargetOwner; + + fn upgrade(&self) -> Option; +} diff --git a/src/criteria/crit_graph/crit_upstream.rs b/src/criteria/crit_graph/crit_upstream.rs new file mode 100644 index 00000000..5042e9a5 --- /dev/null +++ b/src/criteria/crit_graph/crit_upstream.rs @@ -0,0 +1,176 @@ +use { + crate::{ + criteria::{ + CritDestroyListener, CritMatcherId, + crit_graph::{ + WeakCritTargetOwner, + crit_downstream::CritDownstream, + crit_target::{CritTarget, CritTargetOwner}, + }, + crit_per_target_data::CritPerTargetData, + }, + utils::copyhashmap::CopyHashMap, + }, + std::{ + cell::RefMut, + mem, + ops::{Deref, DerefMut}, + rc::{Rc, Weak}, + }, +}; + +pub struct CritUpstreamData +where + Target: CritTarget, +{ + downstream: CopyHashMap>>, + pub nodes: CritPerTargetData>, +} + +pub struct CritUpstreamNodeData +where + Target: CritTarget, +{ + matched: bool, + tl: Target::Owner, + data: T, +} + +pub trait CritUpstreamNodeBase: 'static +where + Target: CritTarget, +{ + type Data; + + fn data(&self) -> &CritUpstreamData; + fn not(&self, mgr: &Target::Mgr) -> Rc>; + fn pull(&self, target: &Target) -> bool; +} + +pub trait CritUpstreamNode: 'static +where + Target: CritTarget, +{ + fn attach(&self, id: CritMatcherId, downstream: Rc>); + 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; +} + +impl CritUpstreamNode for T +where + Target: CritTarget, + T: CritUpstreamNodeBase, +{ + fn attach(&self, id: CritMatcherId, downstream: Rc>) { + let data = self.data(); + for n in data.nodes.borrow_mut().values_mut() { + if !n.matched { + continue; + } + let Some(target) = n.tl.upgrade() else { + continue; + }; + downstream.clone().update_matched(target.data(), true); + } + data.downstream.set(id, Rc::downgrade(&downstream)); + } + + fn detach(&self, id: CritMatcherId) { + self.data().downstream.remove(&id); + } + + fn not(&self, mgr: &Target::Mgr) -> Rc> { + >::not(self, mgr) + } + + fn pull(&self, target: &Target) -> bool { + >::pull(self, target) + } + + fn get(&self, target: &Target) -> bool { + >::data(self).matched(target) + } +} + +impl Deref for CritUpstreamNodeData +where + Target: CritTarget, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for CritUpstreamNodeData +where + Target: CritTarget, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} + +impl CritUpstreamData +where + Target: CritTarget, +{ + pub fn new(slf: &Weak>, id: CritMatcherId) -> Self { + Self { + downstream: Default::default(), + nodes: CritPerTargetData::new(slf, id), + } + } + + pub fn update_matched( + &self, + target: &Target, + mut node: RefMut>, + matched: bool, + remove: bool, + ) { + let unchanged = mem::replace(&mut node.matched, matched) == matched; + drop(node); + if remove { + self.nodes.remove(target.id()); + } + if unchanged { + return; + } + for el in self.downstream.lock().values() { + if let Some(el) = el.upgrade() { + el.update_matched(target, matched); + } + } + } + + pub fn get_or_create(&self, target: &Target) -> RefMut> + where + T: Default, + { + self.nodes.get_or_create(target, || CritUpstreamNodeData { + matched: false, + tl: target.owner(), + data: Default::default(), + }) + } + + pub fn get(&self, target: &Target) -> Option>> { + self.nodes.get(target) + } + + pub fn has_downstream(&self) -> bool { + self.downstream.is_not_empty() + } + + pub fn matched(&self, target: &Target) -> bool { + let Some(node) = self.nodes.get(target) else { + return false; + }; + node.matched + } +} diff --git a/src/criteria/crit_leaf.rs b/src/criteria/crit_leaf.rs new file mode 100644 index 00000000..5bc3ddb4 --- /dev/null +++ b/src/criteria/crit_leaf.rs @@ -0,0 +1,156 @@ +use { + crate::{ + criteria::{ + CritUpstreamNode, + crit_graph::{CritDownstream, CritDownstreamData, CritMgr, CritTarget}, + crit_per_target_data::{CritDestroyListenerBase, CritPerTargetData}, + }, + utils::{cell_ext::CellExt, queue::AsyncQueue}, + }, + std::{ + cell::Cell, + rc::{Rc, Weak}, + }, +}; + +pub struct CritLeafMatcher +where + Target: CritTarget, +{ + upstream: CritDownstreamData, + on_match: Box Box>, + targets: CritPerTargetData>, + events: Rc>>, +} + +pub(in crate::criteria) struct NodeHolder +where + Target: CritTarget, +{ + node: Rc>, +} + +struct Node +where + Target: CritTarget, +{ + leaf: Weak>, + target_id: Target::Id, + needs_event: Cell, + new_data: Cell>, + data: Cell>, + on_unmatch: Cell>>, +} + +pub struct CritLeafEvent +where + Target: CritTarget, +{ + node: Rc>, +} + +impl CritDownstream for CritLeafMatcher +where + Target: CritTarget, +{ + fn update_matched(self: Rc, data: &Target, matched: bool) { + let node = &self + .targets + .get_or_create(data, || { + let node = Rc::new(Node { + leaf: Rc::downgrade(&self), + target_id: data.id(), + needs_event: Cell::new(true), + new_data: Cell::new(None), + data: Cell::new(None), + on_unmatch: Cell::new(None), + }); + NodeHolder { node: node.clone() } + }) + .node; + self.push_event(node, matched.then_some(data.leaf_data())); + } +} + +impl CritLeafMatcher +where + Target: CritTarget, +{ + pub(in crate::criteria) fn new( + mgr: &Target::Mgr, + upstream: &Rc>, + on_match: impl Fn(Target::LeafData) -> Box + 'static, + ) -> Rc { + let id = mgr.id(); + let slf = Rc::new_cyclic(|slf| Self { + targets: CritPerTargetData::new(slf, id), + on_match: Box::new(on_match), + events: mgr.leaf_events().clone(), + upstream: CritDownstreamData::new(id, &[upstream.clone()]), + }); + slf.upstream.attach(&slf); + slf + } + + fn push_event(&self, node: &Rc>, new_data: Option) { + node.new_data.set(new_data); + if node.needs_event.replace(false) { + self.events.push(CritLeafEvent { node: node.clone() }); + } + } +} + +impl CritLeafEvent +where + Target: CritTarget, +{ + #[expect(dead_code)] + pub fn run(self) { + let n = self.node; + n.needs_event.set(true); + if n.new_data != n.data { + if let Some(on_unmatch) = n.on_unmatch.take() { + if n.leaf.strong_count() == 0 { + return; + } + on_unmatch(); + } + } + n.data.set(n.new_data.get()); + if n.data.is_some() != n.on_unmatch.is_some() { + let Some(leaf) = n.leaf.upgrade() else { + return; + }; + if let Some(id) = n.data.get() { + n.on_unmatch.set(Some((leaf.on_match)(id))); + } else { + if let Some(on_unmatch) = n.on_unmatch.take() { + on_unmatch(); + } + leaf.targets.remove(n.target_id); + } + } + } +} + +impl Drop for NodeHolder +where + Target: CritTarget, +{ + fn drop(&mut self) { + if let Some(leaf) = self.node.leaf.upgrade() { + leaf.push_event(&self.node, None); + } + } +} + +impl CritDestroyListenerBase for CritLeafMatcher +where + Target: CritTarget, +{ + type Data = NodeHolder; + + fn data(&self) -> &CritPerTargetData { + &self.targets + } +} diff --git a/src/criteria/crit_matchers.rs b/src/criteria/crit_matchers.rs new file mode 100644 index 00000000..df042f5a --- /dev/null +++ b/src/criteria/crit_matchers.rs @@ -0,0 +1,4 @@ +pub mod critm_any_or_all; +pub mod critm_constant; +pub mod critm_exactly; +pub mod critm_string; diff --git a/src/criteria/crit_matchers/critm_any_or_all.rs b/src/criteria/crit_matchers/critm_any_or_all.rs new file mode 100644 index 00000000..38c3eaaa --- /dev/null +++ b/src/criteria/crit_matchers/critm_any_or_all.rs @@ -0,0 +1,73 @@ +use { + crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + std::{marker::PhantomData, rc::Rc}, +}; + +pub struct CritMatchAnyOrAll +where + Target: CritTarget, +{ + all: bool, + total: usize, + _phantom: PhantomData, +} + +impl CritMatchAnyOrAll +where + Target: CritTarget, +{ + pub fn new(upstream: &[Rc>], all: bool) -> Self { + Self { + all, + total: upstream.len(), + _phantom: Default::default(), + } + } +} + +impl CritMiddleCriterion for CritMatchAnyOrAll +where + Target: CritTarget, +{ + type Data = usize; + type Not = Self; + + fn update_matched(&self, _data: &Target, node: &mut usize, matched: bool) -> bool { + if matched { + *node += 1; + } else { + *node -= 1; + } + if self.all { + *node == self.total + } else { + *node > 0 + } + } + + fn pull(&self, upstream: &[Rc>], node: &Target) -> bool { + for upstream in upstream { + if upstream.pull(node) { + if !self.all { + return true; + } + } else { + if self.all { + return false; + } + } + } + self.all + } + + fn not(&self) -> Self + where + Self: Sized, + { + Self { + all: !self.all, + total: self.total, + _phantom: Default::default(), + } + } +} diff --git a/src/criteria/crit_matchers/critm_constant.rs b/src/criteria/crit_matchers/critm_constant.rs new file mode 100644 index 00000000..c96404bc --- /dev/null +++ b/src/criteria/crit_matchers/critm_constant.rs @@ -0,0 +1,59 @@ +use { + crate::criteria::{ + CritMatcherIds, FixedRootMatcher, + crit_graph::{ + CritFixedRootCriterion, CritFixedRootCriterionBase, CritMgr, CritRoot, CritRootFixed, + CritTarget, + }, + }, + linearize::static_map, + std::{marker::PhantomData, rc::Rc}, +}; + +pub struct CritMatchConstant(pub bool, pub PhantomData); + +impl CritMatchConstant +where + Target: CritTarget, +{ + #[expect(dead_code)] + pub fn create( + roots: &Rc, + ids: &CritMatcherIds, + ) -> FixedRootMatcher> { + static_map! { + v => CritRoot::new( + roots, + ids.next(), + CritRootFixed(Self(v, PhantomData), PhantomData), + ), + } + } +} + +impl CritFixedRootCriterionBase for CritMatchConstant +where + Target: CritTarget, +{ + fn constant(&self) -> bool { + self.0 + } + + fn not<'a>(&self, mgr: &'a Target::Mgr) -> &'a FixedRootMatcher + where + Self: CritFixedRootCriterion, + { + mgr.match_constant() + } +} + +impl CritFixedRootCriterion for CritMatchConstant +where + Target: CritTarget, +{ + const COMPARE: bool = false; + + fn matches(&self, _data: &Target) -> bool { + self.0 + } +} diff --git a/src/criteria/crit_matchers/critm_exactly.rs b/src/criteria/crit_matchers/critm_exactly.rs new file mode 100644 index 00000000..fe4c3e0a --- /dev/null +++ b/src/criteria/crit_matchers/critm_exactly.rs @@ -0,0 +1,61 @@ +use { + crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode}, + std::{marker::PhantomData, rc::Rc}, +}; + +pub struct CritMatchExactly { + total: usize, + num: usize, + not: bool, + _phantom: PhantomData, +} + +impl CritMatchExactly { + pub fn new(upstream: &[Rc>], num: usize) -> Self { + Self { + total: upstream.len(), + num, + not: false, + _phantom: Default::default(), + } + } +} + +impl CritMiddleCriterion for CritMatchExactly +where + Target: CritTarget, +{ + type Data = usize; + type Not = Self; + + fn update_matched(&self, _data: &Target, node: &mut usize, matched: bool) -> bool { + if matched { + *node += 1; + } else { + *node -= 1; + } + (*node == self.num) ^ self.not + } + + fn pull(&self, upstream: &[Rc>], node: &Target) -> bool { + let mut n = 0; + for upstream in upstream { + if upstream.pull(node) { + n += 1; + } + } + (n == self.num) ^ self.not + } + + fn not(&self) -> Self + where + Self: Sized, + { + Self { + total: self.total, + num: self.total - self.num, + not: !self.not, + _phantom: Default::default(), + } + } +} diff --git a/src/criteria/crit_matchers/critm_string.rs b/src/criteria/crit_matchers/critm_string.rs new file mode 100644 index 00000000..b486ea0f --- /dev/null +++ b/src/criteria/crit_matchers/critm_string.rs @@ -0,0 +1,46 @@ +use { + crate::criteria::{ + CritLiteralOrRegex, RootMatcherMap, + crit_graph::{CritRootCriterion, CritTarget}, + }, + std::marker::PhantomData, +}; + +pub struct CritMatchString { + string: CritLiteralOrRegex, + _phantom: PhantomData<(fn(&Target), A)>, +} + +pub trait StringAccess: Sized + 'static +where + Target: CritTarget, +{ + fn with_string(data: &Target, f: impl FnOnce(&str) -> bool) -> bool; + fn nodes( + roots: &Target::RootMatchers, + ) -> &RootMatcherMap>; +} + +impl CritMatchString { + #[expect(dead_code)] + pub fn new(string: CritLiteralOrRegex) -> Self { + Self { + string, + _phantom: Default::default(), + } + } +} + +impl CritRootCriterion for CritMatchString +where + Target: CritTarget, + A: StringAccess, +{ + fn matches(&self, data: &Target) -> bool { + A::with_string(data, |s| self.string.matches(s)) + } + + fn nodes(roots: &Target::RootMatchers) -> Option<&RootMatcherMap> { + Some(A::nodes(roots)) + } +} diff --git a/src/criteria/crit_per_target_data.rs b/src/criteria/crit_per_target_data.rs new file mode 100644 index 00000000..ea9dd86f --- /dev/null +++ b/src/criteria/crit_per_target_data.rs @@ -0,0 +1,136 @@ +use { + crate::criteria::{ + CritMatcherId, + crit_graph::{CritTarget, CritTargetOwner, WeakCritTargetOwner}, + }, + ahash::AHashMap, + std::{ + cell::{RefCell, RefMut}, + ops::{Deref, DerefMut}, + rc::Weak, + }, +}; + +pub struct CritPerTargetData +where + Target: CritTarget, +{ + id: CritMatcherId, + slf: Weak>, + data: RefCell>>, +} + +pub struct PerTreeNodeData +where + Target: CritTarget, +{ + node: Target::Owner, + data: T, +} + +pub(super) trait CritDestroyListenerBase: 'static +where + Target: CritTarget, +{ + type Data; + + fn data(&self) -> &CritPerTargetData; +} + +pub trait CritDestroyListener: 'static +where + Target: CritTarget, +{ + #[expect(dead_code)] + fn destroyed(&self, target_id: Target::Id); +} + +impl CritPerTargetData +where + Target: CritTarget, +{ + pub fn new(slf: &Weak>, id: CritMatcherId) -> Self { + Self { + id, + slf: slf.clone() as _, + data: Default::default(), + } + } + + pub fn get_or_create(&self, target: &Target, default: impl FnOnce() -> T) -> RefMut { + RefMut::map(self.data.borrow_mut(), |d| { + &mut d + .entry(target.id()) + .or_insert_with(|| { + target.destroyed().set(self.id, self.slf.clone()); + PerTreeNodeData { + node: target.owner(), + data: default(), + } + }) + .data + }) + } + + pub fn get(&self, target: &Target) -> Option> { + RefMut::filter_map(self.data.borrow_mut(), |d| { + d.get_mut(&target.id()).map(|d| &mut d.data) + }) + .ok() + } + + pub fn remove(&self, target_id: Target::Id) { + if let Some(node) = self.data.borrow_mut().remove(&target_id) { + if let Some(node) = node.node.upgrade() { + node.data().destroyed().remove(&self.id); + } + } + } + + pub fn borrow_mut(&self) -> RefMut<'_, AHashMap>> { + self.data.borrow_mut() + } +} + +impl Drop for CritPerTargetData +where + Target: CritTarget, +{ + fn drop(&mut self) { + for d in self.data.borrow().values() { + if let Some(n) = d.node.upgrade() { + n.data().destroyed().remove(&self.id); + } + } + } +} + +impl CritDestroyListener for T +where + Target: CritTarget, + T: CritDestroyListenerBase, +{ + fn destroyed(&self, target_id: Target::Id) { + let _v = self.data().data.borrow_mut().remove(&target_id); + } +} + +impl Deref for PerTreeNodeData +where + Target: CritTarget, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for PerTreeNodeData +where + Target: CritTarget, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data + } +} diff --git a/src/macros.rs b/src/macros.rs index c16639b9..65bd71c7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -181,10 +181,10 @@ macro_rules! shared_ids { } macro_rules! linear_ids { - ($ids:ident, $id:ident $(,)?) => { - linear_ids!($ids, $id, u32); + ($(#[$attr1:meta])* $ids:ident, $id:ident $(,)?) => { + linear_ids!($(#[$attr1])* $ids, $id, u32); }; - ($ids:ident, $id:ident, $ty:ty $(,)?) => { + ($(#[$attr1:meta])* $ids:ident, $id:ident, $ty:ty $(,)?) => { pub struct $ids { next: crate::utils::numcell::NumCell<$ty>, } @@ -197,6 +197,7 @@ macro_rules! linear_ids { } } + $(#[$attr1])* impl $ids { pub fn next(&self) -> $id { $id(self.next.fetch_add(1)) diff --git a/src/main.rs b/src/main.rs index 974d0441..4fa74368 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,7 @@ mod cmm; mod compositor; mod config; mod cpu_worker; +mod criteria; mod cursor; mod cursor_user; mod damage; From fd2163d658ee392c5238143f0a3a7e1d8acb5772 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 4 May 2025 18:02:17 +0200 Subject: [PATCH 10/35] config: add client-rule infrastructure --- jay-config/src/_private.rs | 23 +- jay-config/src/_private/client.rs | 191 ++++++++++++- jay-config/src/_private/ipc.rs | 24 +- jay-config/src/client.rs | 87 +++++- src/client.rs | 20 +- src/compositor.rs | 11 + src/config.rs | 4 + src/config/handler.rs | 215 ++++++++++++++- src/criteria.rs | 4 +- src/criteria/clm.rs | 203 ++++++++++++++ src/criteria/clm/clm_matchers.rs | 19 ++ src/criteria/crit_graph/crit_root.rs | 1 - src/criteria/crit_leaf.rs | 1 - src/criteria/crit_matchers/critm_constant.rs | 1 - src/criteria/crit_per_target_data.rs | 1 - src/io_uring.rs | 1 - src/io_uring/debounce.rs | 1 - src/it/test_config.rs | 2 + src/macros.rs | 4 + src/state.rs | 3 + toml-config/src/config.rs | 30 ++ toml-config/src/config/parsers.rs | 2 + toml-config/src/config/parsers/action.rs | 1 + .../src/config/parsers/client_match.rs | 104 +++++++ toml-config/src/config/parsers/client_rule.rs | 104 +++++++ toml-config/src/config/parsers/config.rs | 12 +- .../src/config/parsers/output_match.rs | 2 +- toml-config/src/lib.rs | 56 +++- toml-config/src/rules.rs | 258 ++++++++++++++++++ toml-spec/spec/spec.generated.json | 91 +++++- toml-spec/spec/spec.generated.md | 187 +++++++++++++ toml-spec/spec/spec.yaml | 168 ++++++++++++ 32 files changed, 1804 insertions(+), 27 deletions(-) create mode 100644 src/criteria/clm.rs create mode 100644 src/criteria/clm/clm_matchers.rs create mode 100644 toml-config/src/config/parsers/client_match.rs create mode 100644 toml-config/src/config/parsers/client_rule.rs create mode 100644 toml-config/src/rules.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index cf4b6064..32c28af9 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -4,7 +4,7 @@ mod logging; pub(crate) mod string_error; use { - crate::video::Mode, + crate::{client::ClientMatcher, video::Mode}, bincode::Options, serde::{Deserialize, Serialize}, std::marker::PhantomData, @@ -64,3 +64,24 @@ impl WireMode { pub struct PollableId(pub u64); pub const DEFAULT_SEAT_NAME: &str = "default"; + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum GenericCriterionIpc { + Matcher(T), + Not(T), + List { list: Vec, all: bool }, + Exactly { list: Vec, num: usize }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum ClientCriterionIpc { + Generic(GenericCriterionIpc), + String { + string: String, + field: ClientCriterionStringField, + regex: bool, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum ClientCriterionStringField {} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 7a98bc94..fd0208db 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -3,14 +3,15 @@ use { crate::{ _private::{ - Config, ConfigEntry, ConfigEntryGen, PollableId, VERSION, WireMode, bincode_ops, + ClientCriterionIpc, Config, ConfigEntry, ConfigEntryGen, GenericCriterionIpc, + PollableId, VERSION, WireMode, bincode_ops, ipc::{ ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, }, logging, }, Axis, Direction, ModifiedKeySym, PciId, Workspace, - client::Client, + client::{Client, ClientCriterion, ClientMatcher, MatchedClient}, exec::Command, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, @@ -112,10 +113,16 @@ pub(crate) struct ConfigClient { status_task: Cell>>, i3bar_separator: RefCell>>, pressed_keysym: Cell>, + client_match_handlers: RefCell>, feat_mod_mask: Cell, } +struct ClientMatchHandler { + cb: Callback, + latched: HashMap>, +} + struct Interest { result: Option>, waker: Option, @@ -245,6 +252,7 @@ pub unsafe extern "C" fn init( status_task: Default::default(), i3bar_separator: Default::default(), pressed_keysym: Cell::new(None), + client_match_handlers: Default::default(), feat_mod_mask: Cell::new(false), }); let init = unsafe { slice::from_raw_parts(init, size) }; @@ -280,6 +288,16 @@ macro_rules! get_response { } } +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[non_exhaustive] +enum GenericCriterion<'a, Crit, Matcher> { + Matcher(Matcher), + Not(&'a Crit), + All(&'a [Crit]), + Any(&'a [Crit]), + Exactly(usize, &'a [Crit]), +} + impl ConfigClient { fn send(&self, msg: &ClientMessage) { let mut buf = self.bufs.borrow_mut().pop().unwrap_or_default(); @@ -1419,6 +1437,151 @@ impl ConfigClient { self.send(&ClientMessage::ClientKill { client }); } + fn create_generic_matcher( + &self, + criterion: GenericCriterion<'_, Crit, Matcher>, + child: bool, + create_child_matcher: impl Fn(Crit) -> (Matcher, bool), + create_matcher: impl Fn(GenericCriterionIpc) -> Matcher, + destroy_matcher: impl Fn(Matcher), + ) -> (Matcher, bool) + where + Crit: Copy, + Matcher: Copy, + { + let mut ad_hoc = vec![]; + let mut create_child_matcher = |c: Crit| { + let (m, original) = create_child_matcher(c); + if original { + ad_hoc.push(m); + } + m + }; + let mut create_vec = |l: &[Crit]| { + let mut list = Vec::with_capacity(l.len()); + for c in l { + list.push(create_child_matcher(*c)); + } + list + }; + let criterion = match criterion { + GenericCriterion::Matcher(m) => { + if child { + return (m, false); + } + GenericCriterionIpc::Matcher(m) + } + GenericCriterion::Not(c) => GenericCriterionIpc::Not(create_child_matcher(*c)), + GenericCriterion::All(l) => GenericCriterionIpc::List { + list: create_vec(l), + all: true, + }, + GenericCriterion::Any(l) => GenericCriterionIpc::List { + list: create_vec(l), + all: false, + }, + GenericCriterion::Exactly(num, l) => GenericCriterionIpc::Exactly { + list: create_vec(l), + num, + }, + }; + let matcher = create_matcher(criterion); + for matcher in ad_hoc { + destroy_matcher(matcher); + } + (matcher, true) + } + + pub fn create_client_matcher(&self, criterion: ClientCriterion<'_>) -> ClientMatcher { + self.create_client_matcher_(criterion, false).0 + } + + fn create_client_matcher_( + &self, + criterion: ClientCriterion<'_>, + child: bool, + ) -> (ClientMatcher, bool) { + #[expect(unused_macros)] + macro_rules! string { + ($t:expr, $field:ident, $regex:expr) => { + ClientCriterionIpc::String { + string: $t.to_string(), + field: ClientCriterionStringField::$field, + regex: $regex, + } + }; + } + let create_matcher = |criterion| { + let res = self.send_with_response(&ClientMessage::CreateClientMatcher { + criterion: ClientCriterionIpc::Generic(criterion), + }); + get_response!(res, ClientMatcher(0), CreateClientMatcher { matcher }); + matcher + }; + let destroy_matcher = |matcher| { + self.send(&ClientMessage::DestroyClientMatcher { matcher }); + }; + let generic = |crit: GenericCriterion| { + self.create_generic_matcher::( + crit, + child, + |c| self.create_client_matcher_(c, true), + create_matcher, + destroy_matcher, + ) + }; + #[expect(unused_variables)] + let criterion = match criterion { + ClientCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)), + ClientCriterion::Not(c) => return generic(GenericCriterion::Not(c)), + ClientCriterion::All(c) => return generic(GenericCriterion::All(c)), + ClientCriterion::Any(c) => return generic(GenericCriterion::Any(c)), + ClientCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)), + }; + #[expect(unreachable_code)] + let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion }); + get_response!( + res, + (ClientMatcher(0), false), + CreateClientMatcher { matcher } + ); + (matcher, true) + } + + pub fn set_client_matcher_handler( + &self, + matcher: ClientMatcher, + cb: impl FnMut(MatchedClient) + 'static, + ) { + let cb = Rc::new(RefCell::new(cb)); + let handlers = &mut *self.client_match_handlers.borrow_mut(); + let handler = handlers.entry(matcher).or_insert_with(|| { + self.send(&ClientMessage::EnableClientMatcherEvents { matcher }); + ClientMatchHandler { + cb: cb.clone(), + latched: Default::default(), + } + }); + handler.cb = cb.clone(); + } + + pub fn set_client_matcher_latch_handler( + &self, + matcher: ClientMatcher, + client: Client, + cb: impl FnOnce() + 'static, + ) { + let handlers = &mut *self.client_match_handlers.borrow_mut(); + if let Some(handler) = handlers.get_mut(&matcher) { + handler.latched.insert(client, Box::new(cb)); + } + } + + pub fn destroy_client_matcher(&self, matcher: ClientMatcher) { + self.send(&ClientMessage::DestroyClientMatcher { matcher }); + self.client_match_handlers.borrow_mut().remove(&matcher); + } + fn handle_msg(&self, msg: &[u8]) { self.handle_msg2(msg); self.dispatch_futures(); @@ -1681,6 +1844,30 @@ impl ConfigClient { run_cb("switch event", &cb, event); } } + ServerMessage::ClientMatcherMatched { matcher, client } => { + let cb = { + let handlers = self.client_match_handlers.borrow(); + let Some(handler) = handlers.get(&matcher) else { + return; + }; + handler.cb.clone() + }; + let matched = MatchedClient { matcher, client }; + cb.borrow_mut()(matched); + } + ServerMessage::ClientMatcherUnmatched { matcher, client } => { + let cb = { + let mut handlers = self.client_match_handlers.borrow_mut(); + let Some(handler) = handlers.get_mut(&matcher) else { + return; + }; + let Some(cb) = handler.latched.remove(&client) else { + return; + }; + cb + }; + cb(); + } } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 07b66d54..3f9525ac 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -1,8 +1,8 @@ use { crate::{ - _private::{PollableId, WireMode}, + _private::{ClientCriterionIpc, PollableId, WireMode}, Axis, Direction, PciId, Workspace, - client::Client, + client::{Client, ClientMatcher}, input::{ FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile, capability::Capability, @@ -94,6 +94,14 @@ pub enum ServerMessage { input_device: InputDevice, event: SwitchEvent, }, + ClientMatcherMatched { + matcher: ClientMatcher, + client: Client, + }, + ClientMatcherUnmatched { + matcher: ClientMatcher, + client: Client, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -664,6 +672,15 @@ pub enum ClientMessage<'a> { window: Window, pinned: bool, }, + CreateClientMatcher { + criterion: ClientCriterionIpc, + }, + DestroyClientMatcher { + matcher: ClientMatcher, + }, + EnableClientMatcherEvents { + matcher: ClientMatcher, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -884,6 +901,9 @@ pub enum Response { GetWindowIsVisible { visible: bool, }, + CreateClientMatcher { + matcher: ClientMatcher, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs index 5e908352..e60e100f 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -1,6 +1,9 @@ //! Tools for inspecting and manipulating clients. -use serde::{Deserialize, Serialize}; +use { + serde::{Deserialize, Serialize}, + std::ops::Deref, +}; /// A client connected to the compositor. #[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -34,3 +37,85 @@ impl Client { pub fn clients() -> Vec { get!().clients() } + +/// A client matcher. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct ClientMatcher(pub u64); + +/// A matched client. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct MatchedClient { + pub(crate) matcher: ClientMatcher, + pub(crate) client: Client, +} + +/// A criterion for matching a client. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[non_exhaustive] +pub enum ClientCriterion<'a> { + /// Matches if the contained matcher matches. + Matcher(ClientMatcher), + /// Matches if the contained criterion does not match. + Not(&'a ClientCriterion<'a>), + /// Matches if all of the contained criteria match. + All(&'a [ClientCriterion<'a>]), + /// Matches if any of the contained criteria match. + Any(&'a [ClientCriterion<'a>]), + /// Matches if an exact number of the contained criteria match. + Exactly(usize, &'a [ClientCriterion<'a>]), +} + +impl ClientCriterion<'_> { + /// Converts the criterion to a matcher. + pub fn to_matcher(self) -> ClientMatcher { + get!(ClientMatcher(0)).create_client_matcher(self) + } + + /// Binds a function to execute when the criterion matches a client. + /// + /// This leaks the matcher. + pub fn bind(self, cb: F) { + self.to_matcher().bind(cb); + } +} + +impl ClientMatcher { + /// Destroys the matcher. + /// + /// Any bound callback will no longer be executed. + pub fn destroy(self) { + get!().destroy_client_matcher(self); + } + + /// Sets a function to execute when the criterion matches a client. + /// + /// Replaces any already bound callback. + pub fn bind(self, cb: F) { + get!().set_client_matcher_handler(self, cb); + } +} + +impl MatchedClient { + /// Returns the client that matched. + pub fn client(self) -> Client { + self.client + } + + /// Returns the matcher. + pub fn matcher(self) -> ClientMatcher { + self.matcher + } + + /// Latches a function to be executed when the client no longer matches the criteria. + pub fn latch(self, cb: F) { + get!().set_client_matcher_latch_handler(self.matcher, self.client, cb); + } +} + +impl Deref for MatchedClient { + type Target = Client; + + fn deref(&self) -> &Self::Target { + &self.client + } +} diff --git a/src/client.rs b/src/client.rs index f925a2ea..42075314 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,6 +2,10 @@ use { crate::{ async_engine::SpawnedFuture, client::{error::LookupError, objects::Objects}, + criteria::{ + CritDestroyListener, CritMatcherId, + clm::{CL_CHANGED_DESTROYED, CL_CHANGED_NEW, ClMatcherChange}, + }, ifs::{ wl_display::WlDisplay, wl_registry::WlRegistry, @@ -31,7 +35,7 @@ use { fmt::{Debug, Display, Formatter}, mem, ops::DerefMut, - rc::Rc, + rc::{Rc, Weak}, }, uapi::{OwnedFd, c}, }; @@ -177,6 +181,8 @@ impl Clients { )), wire_scale: Default::default(), focus_stealing_serial: Default::default(), + changed_properties: Default::default(), + destroyed: Default::default(), }); track!(data, data); let display = Rc::new(WlDisplay::new(&data)); @@ -196,6 +202,7 @@ impl Clients { data.pid_info.comm, effective_caps, ); + client.data.property_changed(CL_CHANGED_NEW); self.clients.borrow_mut().insert(client.data.id, client); Ok(data) } @@ -251,6 +258,7 @@ impl Drop for ClientHolder { self.data.surfaces_by_xwayland_serial.clear(); self.data.remove_activation_tokens(); self.data.commit_timelines.clear(); + self.data.property_changed(CL_CHANGED_DESTROYED); if self.data.is_xwayland { if let Some(pidfd) = self.data.state.xwayland.pidfd.get() { if let Err(e) = pidfd_send_signal(&pidfd, c::SIGKILL) { @@ -296,6 +304,8 @@ pub struct Client { pub commit_timelines: Rc, pub wire_scale: Cell>, pub focus_stealing_serial: Cell>, + pub changed_properties: Cell, + pub destroyed: CopyHashMap>>>, } pub const NUM_CACHED_SERIAL_RANGES: usize = 64; @@ -501,6 +511,14 @@ impl Client { self.state.activation_tokens.remove(token); } } + + pub fn property_changed(self: &Rc, change: ClMatcherChange) { + let props = self.changed_properties.get(); + self.changed_properties.set(props | change); + if props.is_none() && change.is_some() { + self.state.cl_matcher_manager.changed(self); + } + } } pub trait WaylandObject: Object { diff --git a/src/compositor.rs b/src/compositor.rs index 7574f20f..b4854958 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -15,6 +15,10 @@ use { cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, config::ConfigProxy, cpu_worker::{CpuWorker, CpuWorkerError}, + criteria::{ + CritMatcherIds, + clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events}, + }, damage::{DamageVisualizer, visualize_damage}, dbus::Dbus, ei::ei_client::EiClients, @@ -156,6 +160,7 @@ fn start_compositor2( scales.add(Scale::from_int(1)); let cpu_worker = Rc::new(CpuWorker::new(&ring, &engine)?); let color_manager = ColorManager::new(); + let crit_ids = Rc::new(CritMatcherIds::default()); let state = Rc::new(State { kb_ctx, backend: CloneCell::new(Rc::new(DummyBackend)), @@ -293,6 +298,7 @@ fn start_compositor2( float_above_fullscreen: Cell::new(false), icons: Default::default(), show_pin_icon: Cell::new(false), + cl_matcher_manager: ClMatcherManager::new(&crit_ids), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -465,6 +471,11 @@ fn start_global_event_handlers( "workspace manager done", workspace_manager_done(state.clone()), ), + eng.spawn("cl matcher manager", handle_cl_changes(state.clone())), + eng.spawn( + "cl matcher leaf events", + handle_cl_leaf_events(state.clone()), + ), ] } diff --git a/src/config.rs b/src/config.rs index c35b90de..ba31e844 100644 --- a/src/config.rs +++ b/src/config.rs @@ -214,6 +214,10 @@ impl ConfigProxy { window_ids: NumCell::new(1), windows_from_tl_id: Default::default(), windows_to_tl_id: Default::default(), + client_matcher_ids: NumCell::new(1), + client_matchers: Default::default(), + client_matcher_cache: Default::default(), + client_matcher_leafs: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 83c1abdf..ace6a277 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -9,6 +9,9 @@ use { cmm::cmm_transfer_function::TransferFunction, compositor::MAX_EXTENTS, config::ConfigProxy, + criteria::{ + CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, clm::ClmLeafMatcher, + }, format::config_formats, ifs::wl_seat::{SeatId, WlSeatGlobal}, io_uring::TaskResultExt, @@ -38,11 +41,11 @@ use { bincode::Options, jay_config::{ _private::{ - PollableId, WireMode, bincode_ops, + ClientCriterionIpc, GenericCriterionIpc, PollableId, WireMode, bincode_ops, ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, - client::Client as ConfigClient, + client::{Client as ConfigClient, ClientMatcher}, input::{ FocusFollowsMouseMode, InputDevice, Seat, acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile}, @@ -65,7 +68,15 @@ use { }, libloading::Library, log::Level, - std::{cell::Cell, ops::Deref, rc::Rc, sync::Arc, time::Duration}, + regex::Regex, + std::{ + cell::Cell, + hash::Hash, + ops::Deref, + rc::{Rc, Weak}, + sync::Arc, + time::Duration, + }, thiserror::Error, uapi::{OwnedFd, c, fcntl_dupfd_cloexec}, }; @@ -97,6 +108,12 @@ pub(super) struct ConfigProxyHandler { pub window_ids: NumCell, pub windows_from_tl_id: CopyHashMap, pub windows_to_tl_id: CopyHashMap, + + pub client_matcher_ids: NumCell, + pub client_matchers: + CopyHashMap>>>, + pub client_matcher_cache: CriterionCache>, + pub client_matcher_leafs: CopyHashMap>, } pub struct Pollable { @@ -113,6 +130,40 @@ pub(super) struct TimerData { _handler: SpawnedFuture<()>, } +pub type CriterionCache = Rc>>>; + +pub struct CachedCriterion +where + K: Hash + Eq, + T: CritTarget, +{ + crit: K, + cache: CriterionCache, + upstream: Vec>>, + node: Rc>, +} + +impl Drop for CachedCriterion +where + K: Hash + Eq, + T: CritTarget, +{ + fn drop(&mut self) { + self.cache.remove(&self.crit); + } +} + +impl CachedCriterion +where + K: Hash + Eq, + T: CritTarget, +{ + #[allow(clippy::allow_attributes, dead_code)] + fn any(&self, v: &impl Fn(&K) -> bool) -> bool { + v(&self.crit) || self.upstream.iter().any(|u| u.any(v)) + } +} + impl ConfigProxyHandler { pub fn do_drop(&self) { self.dropped.set(true); @@ -122,6 +173,9 @@ impl ConfigProxyHandler { self.pollables.clear(); + self.client_matcher_leafs.clear(); + self.client_matchers.clear(); + if let Some(path) = &self.path { if let Err(e) = uapi::unlink(path.as_str()) { log::error!("Could not unlink {}: {}", path, ErrorFmt(OsError(e.0))); @@ -1725,6 +1779,148 @@ impl ConfigProxyHandler { .ok_or(CphError::WindowDoesNotExist(window)) } + fn get_client_matcher( + &self, + matcher: ClientMatcher, + ) -> Result>>, CphError> { + self.client_matchers + .get(&matcher) + .ok_or(CphError::ClientMatcherDoesNotExist(matcher)) + } + + fn sort_generic_matcher( + &self, + generic: &mut GenericCriterionIpc, + key: impl FnMut(&T) -> K, + ) where + K: Ord, + { + match generic { + GenericCriterionIpc::List { list, .. } | GenericCriterionIpc::Exactly { list, .. } => { + list.sort_by_key(key) + } + GenericCriterionIpc::Matcher(_) | GenericCriterionIpc::Not(_) => {} + } + } + + fn create_generic_matcher( + &self, + mgr: &Mgr, + generic: &GenericCriterionIpc, + upstream: &mut Vec>>, + get_matcher: impl Fn(&Matcher) -> Result>, CphError>, + ) -> Result>, CphError> + where + Crit: Clone + Hash + Eq, + Mgr: CritMgrExt, + { + let mut get_upstream = |m: &Matcher| -> Result<_, CphError> { + let m = get_matcher(m)?; + let node = m.node.clone(); + upstream.push(m); + Ok(node) + }; + let node = match generic { + GenericCriterionIpc::Matcher(m) => get_matcher(m)?.node.clone(), + GenericCriterionIpc::Not(m) => mgr.not(&get_upstream(m)?), + GenericCriterionIpc::List { list, all } => { + let mut m = Vec::with_capacity(list.len()); + for c in list { + m.push(get_upstream(c)?); + } + mgr.list(&m, *all) + } + GenericCriterionIpc::Exactly { list, num } => { + let mut m = Vec::with_capacity(list.len()); + for c in list { + m.push(get_upstream(c)?); + } + mgr.exactly(&m, *num) + } + }; + Ok(node) + } + + fn handle_create_client_matcher( + &self, + mut criterion: ClientCriterionIpc, + ) -> Result<(), CphError> { + if let ClientCriterionIpc::Generic(generic) = &mut criterion { + self.sort_generic_matcher(generic, |m| m.0); + } + let id = ClientMatcher(self.client_matcher_ids.fetch_add(1)); + let cache = &self.client_matcher_cache; + if let Some(matcher) = cache.get(&criterion) { + if let Some(matcher) = matcher.upgrade() { + self.client_matchers.set(id, matcher); + self.respond(Response::CreateClientMatcher { matcher: id }); + return Ok(()); + } + } + let mgr = &self.state.cl_matcher_manager; + let mut upstream = vec![]; + let matcher = match &criterion { + ClientCriterionIpc::Generic(m) => { + self.create_generic_matcher(mgr, m, &mut upstream, |m| self.get_client_matcher(*m))? + } + ClientCriterionIpc::String { + string, + field, + regex, + } => { + #[expect(unused_variables)] + let needle = match *regex { + true => { + let regex = Regex::new(string).map_err(CphError::InvalidRegex)?; + CritLiteralOrRegex::Regex(regex) + } + false => CritLiteralOrRegex::Literal(string.to_string()), + }; + match *field {} + } + }; + let cached = Rc::new(CachedCriterion { + crit: criterion.clone(), + cache: cache.clone(), + upstream, + node: matcher.clone(), + }); + cache.set(criterion, Rc::downgrade(&cached)); + self.client_matchers.set(id, cached); + self.respond(Response::CreateClientMatcher { matcher: id }); + Ok(()) + } + + fn handle_destroy_client_matcher(&self, matcher: ClientMatcher) { + self.client_matchers.remove(&matcher); + self.client_matcher_leafs.remove(&matcher); + } + + fn handle_enable_client_matcher_events( + self: &Rc, + matcher: ClientMatcher, + ) -> Result<(), CphError> { + if self.client_matcher_leafs.contains(&matcher) { + return Ok(()); + } + let upstream = self.get_client_matcher(matcher)?; + let slf = self.clone(); + let leaf = self + .state + .cl_matcher_manager + .leaf(&upstream.node, move |id| { + let client = ConfigClient(id.raw()); + slf.send(&ServerMessage::ClientMatcherMatched { matcher, client }); + let slf = slf.clone(); + Box::new(move || { + slf.send(&ServerMessage::ClientMatcherUnmatched { matcher, client }); + }) + }); + self.client_matcher_leafs.set(matcher, leaf); + self.state.cl_matcher_manager.rematch_all(&self.state); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2512,6 +2708,15 @@ impl ConfigProxyHandler { ClientMessage::GetWindowClient { window } => self .handle_get_window_client(window) .wrn("get_window_client")?, + ClientMessage::CreateClientMatcher { criterion } => self + .handle_create_client_matcher(criterion) + .wrn("create_window_matcher")?, + ClientMessage::DestroyClientMatcher { matcher } => { + self.handle_destroy_client_matcher(matcher) + } + ClientMessage::EnableClientMatcherEvents { matcher } => self + .handle_enable_client_matcher_events(matcher) + .wrn("enable_window_matcher_events")?, } Ok(()) } @@ -2593,6 +2798,10 @@ enum CphError { WindowDoesNotExist(Window), #[error("Window {0:?} is not visible")] WindowNotVisible(Window), + #[error("Client matcher {0:?} does not exist")] + ClientMatcherDoesNotExist(ClientMatcher), + #[error("Could not parse regex")] + InvalidRegex(#[source] regex::Error), } trait WithRequestName { diff --git a/src/criteria.rs b/src/criteria.rs index 5e6f21f2..2da4f95b 100644 --- a/src/criteria.rs +++ b/src/criteria.rs @@ -1,3 +1,4 @@ +pub mod clm; mod crit_graph; pub mod crit_leaf; mod crit_matchers; @@ -27,7 +28,6 @@ type RootMatcherMap = CopyHashMap = StaticMap>>>; #[derive(Clone)] -#[expect(dead_code)] pub enum CritLiteralOrRegex { Literal(String), Regex(Regex), @@ -42,7 +42,6 @@ impl CritLiteralOrRegex { } } -#[expect(dead_code)] pub trait CritMgrExt: CritMgr { fn list( &self, @@ -85,6 +84,7 @@ pub trait CritMgrExt: CritMgr { upstream.not(self) } + #[expect(dead_code)] fn root(&self, criterion: T) -> Rc> where T: CritRootCriterion, diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs new file mode 100644 index 00000000..19538ef0 --- /dev/null +++ b/src/criteria/clm.rs @@ -0,0 +1,203 @@ +pub mod clm_matchers; + +use { + crate::{ + client::{Client, ClientId}, + criteria::{ + CritDestroyListener, CritMatcherId, CritMatcherIds, CritUpstreamNode, FixedRootMatcher, + RootMatcherMap, + crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, + crit_leaf::{CritLeafEvent, CritLeafMatcher}, + crit_matchers::critm_constant::CritMatchConstant, + }, + state::State, + utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue}, + }, + std::rc::{Rc, Weak}, +}; + +bitflags! { + ClMatcherChange: u32; + CL_CHANGED_DESTROYED = 1 << 0, + CL_CHANGED_NEW = 1 << 1, +} + +type ClmFixedRootMatcher = FixedRootMatcher, T>; + +pub struct ClMatcherManager { + ids: Rc, + changes: AsyncQueue>, + leaf_events: Rc>>>, + constant: ClmFixedRootMatcher>>, + matchers: Rc, +} + +#[expect(dead_code)] +type ClmRootMatcherMap = RootMatcherMap, T>; + +#[derive(Default)] +pub struct RootMatchers {} + +pub async fn handle_cl_changes(state: Rc) { + let mgr = &state.cl_matcher_manager; + loop { + let tl = mgr.changes.pop().await; + mgr.update_matches(&tl); + } +} + +pub async fn handle_cl_leaf_events(state: Rc) { + let mgr = &state.cl_matcher_manager; + let debouncer = state.ring.debouncer(1000); + loop { + let event = mgr.leaf_events.pop().await; + event.run(); + debouncer.debounce().await; + } +} + +#[expect(dead_code)] +pub type ClmUpstreamNode = dyn CritUpstreamNode>; +pub type ClmLeafMatcher = CritLeafMatcher>; + +impl ClMatcherManager { + pub fn new(ids: &Rc) -> Self { + let matchers = Rc::new(RootMatchers::default()); + #[expect(unused_macros)] + macro_rules! bool { + ($name:ident) => {{ + static_map! { + v => CritRoot::new( + &matchers, + ids.next(), + CritRootFixed($name(v), PhantomData), + ) + } + }}; + } + Self { + constant: CritMatchConstant::create(&matchers, ids), + changes: Default::default(), + leaf_events: Default::default(), + ids: ids.clone(), + matchers, + } + } + + pub fn clear(&self) { + self.changes.clear(); + self.leaf_events.clear(); + } + + pub fn rematch_all(&self, state: &Rc) { + for client in state.clients.clients.borrow().values() { + client.data.property_changed(CL_CHANGED_NEW); + } + } + + pub fn changed(&self, client: &Rc) { + self.changes.push(client.clone()); + } + + fn update_matches(&self, data: &Rc) { + let mut changed = data.changed_properties.take(); + if changed.contains(CL_CHANGED_DESTROYED) { + for destroyed in data.destroyed.lock().drain_values() { + if let Some(destroyed) = destroyed.upgrade() { + destroyed.destroyed(data.id); + } + } + return; + } + #[expect(unused_macros)] + macro_rules! handlers { + ($name:ident) => { + self.matchers + .$name + .lock() + .values() + .filter_map(|m| m.upgrade()) + }; + } + #[expect(unused_macros)] + macro_rules! fixed { + ($name:ident) => { + self.$name[false].handle(data); + self.$name[true].handle(data); + }; + } + if changed.contains(CL_CHANGED_NEW) { + changed |= ClMatcherChange::all(); + #[expect(unused_macros)] + macro_rules! unconditional { + ($field:ident) => { + for m in handlers!($field) { + m.handle(data); + } + }; + } + self.constant[true].handle(data); + } + } +} + +impl CritTarget for Rc { + type Id = ClientId; + type Mgr = ClMatcherManager; + type RootMatchers = RootMatchers; + type LeafData = ClientId; + type Owner = Weak; + + fn owner(&self) -> Self::Owner { + Rc::downgrade(self) + } + + fn id(&self) -> Self::Id { + self.id + } + + fn destroyed(&self) -> &CopyHashMap>> { + &self.destroyed + } + + fn leaf_data(&self) -> Self::LeafData { + self.id + } +} + +impl CritTargetOwner for Rc { + type Target = Rc; + + fn data(&self) -> &Self::Target { + self + } +} + +impl WeakCritTargetOwner for Weak { + type Target = Rc; + type Owner = Rc; + + fn upgrade(&self) -> Option { + self.upgrade() + } +} + +impl CritMgr for ClMatcherManager { + type Target = Rc; + + fn id(&self) -> CritMatcherId { + self.ids.next() + } + + fn leaf_events(&self) -> &Rc>> { + &self.leaf_events + } + + fn match_constant(&self) -> &FixedRootMatcher> { + &self.constant + } + + fn roots(&self) -> &Rc<::RootMatchers> { + &self.matchers + } +} diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs new file mode 100644 index 00000000..246a4f9c --- /dev/null +++ b/src/criteria/clm/clm_matchers.rs @@ -0,0 +1,19 @@ +#[expect(unused_macros)] +macro_rules! fixed_root_criterion { + ($ty:ty, $field:ident) => { + impl crate::criteria::crit_graph::CritFixedRootCriterionBase> + for $ty + { + fn constant(&self) -> bool { + self.0 + } + + fn not<'a>( + &self, + mgr: &'a crate::criteria::clm::ClMatcherManager, + ) -> &'a crate::criteria::FixedRootMatcher, Self> { + &mgr.$field + } + } + }; +} diff --git a/src/criteria/crit_graph/crit_root.rs b/src/criteria/crit_graph/crit_root.rs index 20828765..9992f40d 100644 --- a/src/criteria/crit_graph/crit_root.rs +++ b/src/criteria/crit_graph/crit_root.rs @@ -129,7 +129,6 @@ where slf } - #[expect(dead_code)] pub fn handle(&self, target: &Target) { let new = self.criterion.matches(target) ^ self.not; let node = match new { diff --git a/src/criteria/crit_leaf.rs b/src/criteria/crit_leaf.rs index 5bc3ddb4..68a4cd7a 100644 --- a/src/criteria/crit_leaf.rs +++ b/src/criteria/crit_leaf.rs @@ -104,7 +104,6 @@ impl CritLeafEvent where Target: CritTarget, { - #[expect(dead_code)] pub fn run(self) { let n = self.node; n.needs_event.set(true); diff --git a/src/criteria/crit_matchers/critm_constant.rs b/src/criteria/crit_matchers/critm_constant.rs index c96404bc..b45eea19 100644 --- a/src/criteria/crit_matchers/critm_constant.rs +++ b/src/criteria/crit_matchers/critm_constant.rs @@ -16,7 +16,6 @@ impl CritMatchConstant where Target: CritTarget, { - #[expect(dead_code)] pub fn create( roots: &Rc, ids: &CritMatcherIds, diff --git a/src/criteria/crit_per_target_data.rs b/src/criteria/crit_per_target_data.rs index ea9dd86f..506d8480 100644 --- a/src/criteria/crit_per_target_data.rs +++ b/src/criteria/crit_per_target_data.rs @@ -41,7 +41,6 @@ pub trait CritDestroyListener: 'static where Target: CritTarget, { - #[expect(dead_code)] fn destroyed(&self, target_id: Target::Id); } diff --git a/src/io_uring.rs b/src/io_uring.rs index d5e1a249..eef3933b 100644 --- a/src/io_uring.rs +++ b/src/io_uring.rs @@ -266,7 +266,6 @@ impl IoUring { self.ring.cancel_task(id); } - #[expect(dead_code)] pub fn debouncer(&self, max: u64) -> Debouncer { Debouncer { cur: Default::default(), diff --git a/src/io_uring/debounce.rs b/src/io_uring/debounce.rs index 6c68693f..f5b65f40 100644 --- a/src/io_uring/debounce.rs +++ b/src/io_uring/debounce.rs @@ -11,7 +11,6 @@ pub struct Debouncer { } impl Debouncer { - #[expect(dead_code)] pub async fn debounce(&self) { let iteration = self.ring.iteration.get(); if self.iteration.replace(iteration) != iteration { diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 12c086d7..621073ca 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -124,6 +124,8 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { ServerMessage::InterestReady { .. } => {} ServerMessage::Features { .. } => {} ServerMessage::SwitchEvent { .. } => {} + ServerMessage::ClientMatcherMatched { .. } => {} + ServerMessage::ClientMatcherUnmatched { .. } => {} } } diff --git a/src/macros.rs b/src/macros.rs index 65bd71c7..ad0d6121 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -475,6 +475,10 @@ macro_rules! bitflags { self.0 != 0 } + pub fn is_none(self) -> bool { + self.0 == 0 + } + pub fn all() -> Self { Self(0 $(| $val)*) } diff --git a/src/state.rs b/src/state.rs index 3c96c63b..2af9769a 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,6 +15,7 @@ use { compositor::LIBEI_SOCKET, config::ConfigProxy, cpu_worker::CpuWorker, + criteria::clm::ClMatcherManager, cursor::{Cursor, ServerCursors}, cursor_user::{CursorUserGroup, CursorUserGroupId, CursorUserGroupIds, CursorUserIds}, damage::DamageVisualizer, @@ -241,6 +242,7 @@ pub struct State { pub float_above_fullscreen: Cell, pub icons: Icons, pub show_pin_icon: Cell, + pub cl_matcher_manager: ClMatcherManager, } // impl Drop for State { @@ -949,6 +951,7 @@ impl State { self.slow_ei_clients.clear(); self.toplevels.clear(); self.workspace_managers.clear(); + self.cl_matcher_manager.clear(); } pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 7b4a91c5..d01cbc93 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -66,6 +66,7 @@ pub enum SimpleCommand { ToggleFloatAboveFullscreen, SetFloatPinned(bool), ToggleFloatPinned, + KillClient, } #[derive(Debug, Clone)] @@ -198,6 +199,34 @@ pub enum OutputMatch { }, } +#[derive(Default, Debug, Clone)] +pub struct GenericMatch { + pub name: Option, + pub not: Option>, + pub all: Option>, + pub any: Option>, + pub exactly: Option>, +} + +#[derive(Debug, Clone)] +pub struct MatchExactly { + pub num: usize, + pub list: Vec, +} + +#[derive(Debug, Clone)] +pub struct ClientRule { + pub name: Option, + pub match_: ClientMatch, + pub action: Option, + pub latch: Option, +} + +#[derive(Default, Debug, Clone)] +pub struct ClientMatch { + pub generic: GenericMatch, +} + #[derive(Debug, Clone)] pub enum DrmDeviceMatch { Any(Vec), @@ -395,6 +424,7 @@ pub struct Config { pub float: Option, pub named_actions: Vec, pub max_action_depth: u64, + pub client_rules: Vec, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index 0112410b..ca1dc2e0 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -8,6 +8,8 @@ use { pub mod action; mod actions; +mod client_match; +mod client_rule; mod color; pub mod color_management; pub mod config; diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 4457f42b..618c0cb1 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -132,6 +132,7 @@ impl ActionParser<'_> { "pin-float" => SetFloatPinned(true), "unpin-float" => SetFloatPinned(false), "toggle-float-pinned" => ToggleFloatPinned, + "kill-client" => KillClient, _ => { return Err( ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) diff --git a/toml-config/src/config/parsers/client_match.rs b/toml-config/src/config/parsers/client_match.rs new file mode 100644 index 00000000..b3a1ca54 --- /dev/null +++ b/toml-config/src/config/parsers/client_match.rs @@ -0,0 +1,104 @@ +use { + crate::{ + config::{ + ClientMatch, GenericMatch, MatchExactly, + context::Context, + extractor::{Extractor, ExtractorError, arr, n32, opt, str, val}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum ClientMatchParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct ClientMatchParser<'a>(pub &'a Context<'a>); + +impl Parser for ClientMatchParser<'_> { + type Value = ClientMatch; + type Error = ClientMatchParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let ((name, not_val, all_val, any_val, exactly_val),) = ext.extract((( + opt(str("name")), + opt(val("not")), + opt(arr("all")), + opt(arr("any")), + opt(val("exactly")), + ),))?; + let mut not = None; + if let Some(value) = not_val { + not = Some(Box::new(value.parse(&mut ClientMatchParser(self.0))?)); + } + macro_rules! list { + ($val:expr) => {{ + let mut list = None; + if let Some(value) = $val { + let mut res = vec![]; + for value in value.value { + res.push(value.parse(&mut ClientMatchParser(self.0))?); + } + list = Some(res); + } + list + }}; + } + let all = list!(all_val); + let any = list!(any_val); + let mut exactly = None; + if let Some(value) = exactly_val { + exactly = Some(value.parse(&mut ClientMatchExactlyParser(self.0))?); + } + Ok(ClientMatch { + generic: GenericMatch { + name: name.despan_into(), + not, + all, + any, + exactly, + }, + }) + } +} + +pub struct ClientMatchExactlyParser<'a>(pub &'a Context<'a>); + +impl Parser for ClientMatchExactlyParser<'_> { + type Value = MatchExactly; + type Error = ClientMatchParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (num, list_val) = ext.extract((n32("num"), arr("list")))?; + let mut list = vec![]; + for el in list_val.value { + list.push(el.parse(&mut ClientMatchParser(self.0))?); + } + Ok(MatchExactly { + num: num.value as _, + list, + }) + } +} diff --git a/toml-config/src/config/parsers/client_rule.rs b/toml-config/src/config/parsers/client_rule.rs new file mode 100644 index 00000000..50bf7f0d --- /dev/null +++ b/toml-config/src/config/parsers/client_rule.rs @@ -0,0 +1,104 @@ +use { + crate::{ + config::{ + ClientMatch, ClientRule, + context::Context, + extractor::{Extractor, ExtractorError, opt, str, val}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + parsers::{ + action::{ActionParser, ActionParserError}, + client_match::{ClientMatchParser, ClientMatchParserError}, + }, + spanned::SpannedErrorExt, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum ClientRuleParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error(transparent)] + Match(#[from] ClientMatchParserError), + #[error(transparent)] + Action(ActionParserError), + #[error(transparent)] + Latch(ActionParserError), +} + +pub struct ClientRuleParser<'a>(pub &'a Context<'a>); + +impl Parser for ClientRuleParser<'_> { + type Value = ClientRule; + type Error = ClientRuleParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (name, match_val, action_val, latch_val) = ext.extract(( + opt(str("name")), + opt(val("match")), + opt(val("action")), + opt(val("latch")), + ))?; + let mut action = None; + if let Some(value) = action_val { + action = Some( + value + .parse(&mut ActionParser(self.0)) + .map_spanned_err(ClientRuleParserError::Action)?, + ); + } + let mut latch = None; + if let Some(value) = latch_val { + latch = Some( + value + .parse(&mut ActionParser(self.0)) + .map_spanned_err(ClientRuleParserError::Latch)?, + ); + } + let match_ = match match_val { + None => ClientMatch::default(), + Some(m) => m.parse_map(&mut ClientMatchParser(self.0))?, + }; + Ok(ClientRule { + name: name.despan_into(), + match_, + action, + latch, + }) + } +} + +pub struct ClientRulesParser<'a>(pub &'a Context<'a>); + +impl Parser for ClientRulesParser<'_> { + type Value = Vec; + type Error = ClientRuleParserError; + const EXPECTED: &'static [DataType] = &[DataType::Array]; + + fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { + let mut res = vec![]; + for el in array { + match el.parse(&mut ClientRuleParser(self.0)) { + Ok(o) => res.push(o), + Err(e) => { + log::warn!("Could not parse client rule: {}", self.0.error(e)); + } + } + } + Ok(res) + } +} diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 98520a14..4baf4d36 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -8,6 +8,7 @@ use { parsers::{ action::ActionParser, actions::ActionsParser, + client_rule::ClientRulesParser, color_management::ColorManagementParser, connector::ConnectorsParser, drm_device::DrmDevicesParser, @@ -120,7 +121,7 @@ impl Parser for ConfigParser<'_> { ui_drag_val, xwayland_val, ), - (color_management_val, float_val, actions_val, max_action_depth_val), + (color_management_val, float_val, actions_val, max_action_depth_val, client_rules_val), ) = ext.extract(( ( opt(val("keymap")), @@ -163,6 +164,7 @@ impl Parser for ConfigParser<'_> { opt(val("float")), opt(val("actions")), recover(opt(int("max-action-depth"))), + opt(val("clients")), ), ))?; let mut keymap = None; @@ -419,6 +421,13 @@ impl Parser for ConfigParser<'_> { } max_action_depth = value.value as _; } + let mut client_rules = vec![]; + if let Some(value) = client_rules_val { + match value.parse(&mut ClientRulesParser(self.0)) { + Ok(v) => client_rules = v, + Err(e) => log::warn!("Could not parse the client rules: {}", self.0.error(e)), + } + } Ok(Config { keymap, repeat_rate, @@ -453,6 +462,7 @@ impl Parser for ConfigParser<'_> { float, named_actions, max_action_depth, + client_rules, }) } } diff --git a/toml-config/src/config/parsers/output_match.rs b/toml-config/src/config/parsers/output_match.rs index f771135a..4af292f2 100644 --- a/toml-config/src/config/parsers/output_match.rs +++ b/toml-config/src/config/parsers/output_match.rs @@ -28,7 +28,7 @@ pub struct OutputMatchParser<'a>(pub &'a Context<'a>); impl Parser for OutputMatchParser<'_> { type Value = OutputMatch; type Error = OutputMatchParserError; - const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Table]; + const EXPECTED: &'static [DataType] = &[DataType::Table, DataType::Array]; fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { let mut res = vec![]; diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index f19cc546..cd3afb22 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -1,17 +1,27 @@ -#![allow(clippy::len_zero, clippy::single_char_pattern, clippy::collapsible_if)] +#![allow( + clippy::len_zero, + clippy::single_char_pattern, + clippy::collapsible_if, + clippy::collapsible_else_if +)] mod config; +mod rules; mod toml; use { - crate::config::{ - Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, - DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut, SimpleCommand, - Status, Theme, parse_config, + crate::{ + config::{ + Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, + ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut, + SimpleCommand, Status, Theme, parse_config, + }, + rules::{MatcherTemp, RuleMapper}, }, ahash::{AHashMap, AHashSet}, error_reporter::Report, jay_config::{ + client::Client, config, config_dir, exec::{Command, set_env, unset_env}, get_workspace, @@ -79,6 +89,16 @@ impl Action { } fn into_fn_impl(self, state: &Rc) -> B { + macro_rules! client_action { + ($name:ident, $opt:expr) => {{ + let state = state.clone(); + B::new(move || { + if let Some($name) = state.client.get() { + $opt + } + }) + }}; + } let s = state.persistent.seat; match self { Action::SimpleCommand { cmd } => match cmd { @@ -115,6 +135,7 @@ impl Action { SimpleCommand::ToggleFloatAboveFullscreen => B::new(toggle_float_above_fullscreen), SimpleCommand::SetFloatPinned(pinned) => B::new(move || s.set_float_pinned(pinned)), SimpleCommand::ToggleFloatPinned => B::new(move || s.toggle_float_pinned()), + SimpleCommand::KillClient => client_action!(c, c.kill()), }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -666,6 +687,8 @@ struct State { action_depth_max: u64, action_depth: Cell, + + client: Cell>, } impl Drop for State { @@ -871,6 +894,16 @@ impl State { } } } + + fn with_client(&self, client: Client, check: bool, f: impl FnOnce()) { + let mut opt = Some(client); + if check && client.does_not_exist() { + opt = None; + } + self.client.set(opt); + f(); + self.client.set(None); + } } #[derive(Eq, PartialEq, Hash)] @@ -887,6 +920,8 @@ struct PersistentState { binds: RefCell>, #[expect(clippy::type_complexity)] actions: RefCell, Rc>>, + client_rules: Cell>>, + client_rule_mapper: RefCell>>, } fn load_config(initial_load: bool, persistent: &Rc) { @@ -967,7 +1002,11 @@ fn load_config(initial_load: bool, persistent: &Rc) { io_outputs: Default::default(), action_depth_max: config.max_action_depth, action_depth: Cell::new(0), + client: Default::default(), }); + let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules); + persistent.client_rules.set(client_rules); + *state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper); state.set_status(&config.status); persistent.actions.borrow_mut().clear(); for a in config.named_actions { @@ -1190,10 +1229,15 @@ pub fn configure() { seat: default_seat(), binds: Default::default(), actions: Default::default(), + client_rules: Default::default(), + client_rule_mapper: Default::default(), }); { let p = persistent.clone(); - on_unload(move || p.actions.borrow_mut().clear()); + on_unload(move || { + p.actions.borrow_mut().clear(); + p.client_rule_mapper.borrow_mut().take(); + }); } load_config(true, &persistent); } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs new file mode 100644 index 00000000..87eff524 --- /dev/null +++ b/toml-config/src/rules.rs @@ -0,0 +1,258 @@ +use { + crate::{ + State, + config::{ClientMatch, ClientRule, GenericMatch}, + }, + ahash::{AHashMap, AHashSet}, + jay_config::client::{ClientCriterion, ClientMatcher}, + std::{mem::ManuallyDrop, rc::Rc}, +}; + +impl State { + pub fn create_rules(self: &Rc, rules: &[R]) -> (Vec>, RuleMapper) + where + R: Rule, + { + let mut names = AHashMap::new(); + for (idx, rule) in rules.iter().enumerate() { + if let Some(name) = rule.name() { + names.insert(name.to_string(), idx); + } + } + let mut mapper = RuleMapper { + state: self.clone(), + names, + pending: Default::default(), + mapped: Default::default(), + }; + let mut matchers = vec![]; + for idx in 0..rules.len() { + if let Some(matcher) = mapper.map_rule(rules, idx) { + matchers.push(MatcherTemp(matcher)); + } + } + (matchers, mapper) + } +} + +pub trait Rule: Sized + 'static { + type Match; + type Matcher: Copy + 'static; + type Criterion<'a>; + + const NAME_UPPER: &str; + const NAME_LOWER: &str; + + fn name(&self) -> Option<&str>; + fn match_(&self) -> &Self::Match; + fn generic(m: &Self::Match) -> &GenericMatch; + fn map_custom( + state: &Rc, + all: &mut Vec>, + match_: &Self::Match, + ) -> Option<()>; + fn create(c: Self::Criterion<'_>) -> Self::Matcher; + fn destroy(m: Self::Matcher); + fn bind(&self, state: &Rc, matcher: Self::Matcher); + + fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static>; + fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a>; + fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>; + fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>; + fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>; +} + +impl Rule for ClientRule { + type Match = ClientMatch; + type Matcher = ClientMatcher; + type Criterion<'a> = ClientCriterion<'a>; + + const NAME_UPPER: &str = "Client"; + const NAME_LOWER: &str = "client"; + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn match_(&self) -> &Self::Match { + &self.match_ + } + + fn generic(m: &Self::Match) -> &GenericMatch { + &m.generic + } + + fn map_custom( + _state: &Rc, + _all: &mut Vec>, + _match_: &Self::Match, + ) -> Option<()> { + Some(()) + } + + fn create(c: Self::Criterion<'_>) -> Self::Matcher { + c.to_matcher() + } + + fn destroy(m: Self::Matcher) { + m.destroy(); + } + + fn bind(&self, state: &Rc, matcher: Self::Matcher) { + let state = state.clone(); + macro_rules! latch { + ($g:ident, $client:ident) => { + let g = $g.clone(); + let state = state.clone(); + $client.latch(move || { + state.with_client($client.client(), true, || g()); + }); + }; + } + if let Some(action) = &self.action { + let f = action.clone().into_fn(&state); + if let Some(action) = &self.latch { + let g = action.clone().into_rc_fn(&state); + let state = state.clone(); + matcher.bind(move |client| { + state.with_client(client.client(), false, &f); + latch!(g, client); + }); + } else { + matcher.bind(move |client| { + state.with_client(client.client(), false, &f); + }); + } + } else { + if let Some(action) = &self.latch { + let g = action.clone().into_rc_fn(&state); + matcher.bind(move |client| { + latch!(g, client); + }); + } + } + } + + fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { + ClientCriterion::Matcher(m) + } + + fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a> { + ClientCriterion::Not(m) + } + + fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + ClientCriterion::All(m) + } + + fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + ClientCriterion::Any(m) + } + + fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + ClientCriterion::Exactly(n, m) + } +} + +pub struct RuleMapper +where + R: Rule, +{ + state: Rc, + names: AHashMap, + pending: AHashSet, + mapped: AHashMap, +} + +pub struct MatcherTemp(R::Matcher) +where + R: Rule; + +impl Drop for MatcherTemp +where + R: Rule, +{ + fn drop(&mut self) { + R::destroy(self.0); + } +} + +impl RuleMapper +where + R: Rule, +{ + fn map_rule(&mut self, rules: &[R], idx: usize) -> Option { + if let Some(matcher) = self.mapped.get(&idx) { + return Some(*matcher); + } + if !self.pending.insert(idx) { + if let Some(name) = rules.get(idx).and_then(|r| r.name()) { + log::error!("{} rule `{name}` has a loop", R::NAME_UPPER); + } + return None; + } + let rule = &rules[idx]; + let matcher = self.map_match(rules, rule.match_())?; + self.mapped.insert(idx, matcher); + rule.bind(&self.state, matcher); + Some(matcher) + } + + fn map_temporary_match(&mut self, rules: &[R], matcher: &R::Match) -> Option> { + self.map_match(rules, matcher).map(MatcherTemp) + } + + fn map_match(&mut self, rules: &[R], matcher: &R::Match) -> Option { + let mut all = vec![]; + self.map_generic_match(rules, &mut all, R::generic(matcher))?; + R::map_custom(&self.state, &mut all, matcher)?; + if all.len() == 1 { + return Some(ManuallyDrop::new(all.pop().unwrap()).0); + } + let all: Vec<_> = all.iter().map(|m| R::gen_matcher(m.0)).collect(); + Some(R::create(R::gen_all(&all))) + } + + fn map_generic_match( + &mut self, + rules: &[R], + all: &mut Vec>, + matcher: &GenericMatch, + ) -> Option<()> { + let m = |c: R::Criterion<'_>| MatcherTemp(R::create(c)); + if let Some(name) = &matcher.name { + let Some(&idx) = self.names.get(&**name) else { + log::error!("There is no {} rule named `{name}`", R::NAME_LOWER); + return None; + }; + let matcher = self.map_rule(rules, idx)?; + all.push(m(R::gen_matcher(matcher))); + } + if let Some(not) = &matcher.not { + let matcher = self.map_temporary_match(rules, not)?; + all.push(m(R::gen_not(&R::gen_matcher(matcher.0)))); + } + if let Some(list) = &matcher.all { + for match_ in list { + all.push(self.map_temporary_match(rules, match_)?); + } + } + if let Some(list) = &matcher.any { + let mut any = vec![]; + for match_ in list { + any.push(self.map_temporary_match(rules, match_)?); + } + let any: Vec<_> = any.iter().map(|m| R::gen_matcher(m.0)).collect(); + all.push(m(R::gen_any(&any))); + } + if let Some(exactly) = &matcher.exactly { + let mut list = vec![]; + for match_ in &exactly.list { + list.push(self.map_temporary_match(rules, match_)?); + } + let list: Vec<_> = list.iter().map(|m| R::gen_matcher(m.0)).collect(); + all.push(m(R::gen_exactly(exactly.num, &list))) + } + Some(()) + } +} diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1f05aa76..eef59221 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -500,6 +500,86 @@ } ] }, + "ClientMatch": { + "description": "Criteria for matching clients.\n\nIf no fields are set, all clients are matched. If multiple fields are set, all fields\nmust match the client.\n", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Matches if the client rule with this name matches.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n\n # Matches the same clients as the previous rule.\n [[clients]]\n match.name = \"spotify\"\n ```\n" + }, + "not": { + "description": "Matches if the contained criteria don't match.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"not-spotify\"\n match.not.sandbox-app-id = \"com.spotify.Client\"\n ```\n", + "$ref": "#/$defs/ClientMatch" + }, + "all": { + "type": "array", + "description": "Matches if all of the contained criteria match.\n\n- Example:\n\n ```toml\n [[clients]]\n match.all = [\n { sandbox-app-id = \"com.spotify.Client\" },\n { sandbox-engine = \"org.flatpak\" },\n ]\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/ClientMatch" + } + }, + "any": { + "type": "array", + "description": "Matches if any of the contained criteria match.\n\n- Example:\n\n ```toml\n [[clients]]\n match.any = [\n { sandbox-app-id = \"com.spotify.Client\" },\n { sandbox-app-id = \"com.valvesoftware.Steam\" },\n ]\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/ClientMatch" + } + }, + "exactly": { + "description": "Matches if a specific number of contained criteria match.\n\n- Example:\n\n ```toml\n # Matches any client that is either steam or sandboxed by flatpak but not both.\n [[clients]]\n match.exactly.num = 1\n match.exactly.list = [\n { sandbox-engine = \"org.flatpak\" },\n { sandbox-app-id = \"com.valvesoftware.Steam\" },\n ]\n ```\n", + "$ref": "#/$defs/ClientMatchExactly" + } + }, + "required": [] + }, + "ClientMatchExactly": { + "description": "Criterion for matching a specific number of client criteria.\n", + "type": "object", + "properties": { + "num": { + "type": "number", + "description": "The number of criteria that must match." + }, + "list": { + "type": "array", + "description": "The list of criteria.", + "items": { + "description": "", + "$ref": "#/$defs/ClientMatch" + } + } + }, + "required": [ + "num", + "list" + ] + }, + "ClientRule": { + "description": "A client rule.\n", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of this rule.\n\nThis name can be referenced in other rules.\n\n- Example\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n\n [[clients]]\n match.name = \"spotify\"\n action = \"kill-client\"\n ```\n" + }, + "match": { + "description": "The criteria that select the client that this rule applies to.", + "$ref": "#/$defs/ClientMatch" + }, + "action": { + "description": "An action to execute when a client matches the criteria.", + "$ref": "#/$defs/Action" + }, + "latch": { + "description": "An action to execute when a client no longer matches the criteria.", + "$ref": "#/$defs/Action" + } + }, + "required": [] + }, "Color": { "type": "string", "description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n" @@ -714,6 +794,14 @@ "type": "integer", "description": "The maximum call depth of named actions. This setting prevents infinite recursion\nwhen using named actions. Setting this value to 0 or less disables named actions\ncompletely. The default is `16`.\n", "minimum": 0.0 + }, + "clients": { + "type": "array", + "description": "An array of client rules.\n\nThese rules can be used to give names to clients and to manipulate them.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/ClientRule" + } } }, "required": [] @@ -1384,7 +1472,8 @@ "toggle-float-above-fullscreen", "pin-float", "unpin-float", - "toggle-float-pinned" + "toggle-float-pinned", + "kill-client" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index aeca6373..1c5b44a0 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -700,6 +700,171 @@ The string should have one of the following values: The brightness in cd/m^2. + +### `ClientMatch` + +Criteria for matching clients. + +If no fields are set, all clients are matched. If multiple fields are set, all fields +must match the client. + +Values of this type should be tables. + +The table has the following fields: + +- `name` (optional): + + Matches if the client rule with this name matches. + + - Example: + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + + # Matches the same clients as the previous rule. + [[clients]] + match.name = "spotify" + ``` + + The value of this field should be a string. + +- `not` (optional): + + Matches if the contained criteria don't match. + + - Example: + + ```toml + [[clients]] + name = "not-spotify" + match.not.sandbox-app-id = "com.spotify.Client" + ``` + + The value of this field should be a [ClientMatch](#types-ClientMatch). + +- `all` (optional): + + Matches if all of the contained criteria match. + + - Example: + + ```toml + [[clients]] + match.all = [ + { sandbox-app-id = "com.spotify.Client" }, + { sandbox-engine = "org.flatpak" }, + ] + ``` + + The value of this field should be an array of [ClientMatchs](#types-ClientMatch). + +- `any` (optional): + + Matches if any of the contained criteria match. + + - Example: + + ```toml + [[clients]] + match.any = [ + { sandbox-app-id = "com.spotify.Client" }, + { sandbox-app-id = "com.valvesoftware.Steam" }, + ] + ``` + + The value of this field should be an array of [ClientMatchs](#types-ClientMatch). + +- `exactly` (optional): + + Matches if a specific number of contained criteria match. + + - Example: + + ```toml + # Matches any client that is either steam or sandboxed by flatpak but not both. + [[clients]] + match.exactly.num = 1 + match.exactly.list = [ + { sandbox-engine = "org.flatpak" }, + { sandbox-app-id = "com.valvesoftware.Steam" }, + ] + ``` + + The value of this field should be a [ClientMatchExactly](#types-ClientMatchExactly). + + + +### `ClientMatchExactly` + +Criterion for matching a specific number of client criteria. + +Values of this type should be tables. + +The table has the following fields: + +- `num` (required): + + The number of criteria that must match. + + The value of this field should be a number. + +- `list` (required): + + The list of criteria. + + The value of this field should be an array of [ClientMatchs](#types-ClientMatch). + + + +### `ClientRule` + +A client rule. + +Values of this type should be tables. + +The table has the following fields: + +- `name` (optional): + + The name of this rule. + + This name can be referenced in other rules. + + - Example + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + + [[clients]] + match.name = "spotify" + action = "kill-client" + ``` + + The value of this field should be a string. + +- `match` (optional): + + The criteria that select the client that this rule applies to. + + The value of this field should be a [ClientMatch](#types-ClientMatch). + +- `action` (optional): + + An action to execute when a client matches the criteria. + + The value of this field should be a [Action](#types-Action). + +- `latch` (optional): + + An action to execute when a client no longer matches the criteria. + + The value of this field should be a [Action](#types-Action). + + ### `Color` @@ -1417,6 +1582,22 @@ The table has the following fields: The numbers should be greater than or equal to 0. +- `clients` (optional): + + An array of client rules. + + These rules can be used to give names to clients and to manipulate them. + + - Example: + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + ``` + + The value of this field should be an array of [ClientRules](#types-ClientRule). + ### `Connector` @@ -3129,6 +3310,12 @@ The string should have one of the following values: Toggles whether the currently focused floating window is pinned. +- `kill-client`: + + Kills a client. + + This action has no effect outside of client rules. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 50761da3..e44eac62 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -821,6 +821,11 @@ SimpleActionName: - value: toggle-float-pinned description: | Toggles whether the currently focused floating window is pinned. + - value: kill-client + description: | + Kills a client. + + This action has no effect outside of client rules. Color: @@ -2487,6 +2492,23 @@ Config: The maximum call depth of named actions. This setting prevents infinite recursion when using named actions. Setting this value to 0 or less disables named actions completely. The default is `16`. + clients: + kind: array + items: + ref: ClientRule + required: false + description: | + An array of client rules. + + These rules can be used to give names to clients and to manipulate them. + + - Example: + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + ``` Idle: @@ -3016,3 +3038,149 @@ Float: The default is `false`. kind: boolean required: false + + +ClientRule: + kind: table + description: | + A client rule. + fields: + name: + kind: string + required: false + description: | + The name of this rule. + + This name can be referenced in other rules. + + - Example + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + + [[clients]] + match.name = "spotify" + action = "kill-client" + ``` + match: + ref: ClientMatch + required: false + description: The criteria that select the client that this rule applies to. + action: + ref: Action + required: false + description: An action to execute when a client matches the criteria. + latch: + ref: Action + required: false + description: An action to execute when a client no longer matches the criteria. + + +ClientMatch: + kind: table + description: | + Criteria for matching clients. + + If no fields are set, all clients are matched. If multiple fields are set, all fields + must match the client. + fields: + name: + kind: string + required: false + description: | + Matches if the client rule with this name matches. + + - Example: + + ```toml + [[clients]] + name = "spotify" + match.sandbox-app-id = "com.spotify.Client" + + # Matches the same clients as the previous rule. + [[clients]] + match.name = "spotify" + ``` + not: + ref: ClientMatch + required: false + description: | + Matches if the contained criteria don't match. + + - Example: + + ```toml + [[clients]] + name = "not-spotify" + match.not.sandbox-app-id = "com.spotify.Client" + ``` + all: + kind: array + items: + ref: ClientMatch + required: false + description: | + Matches if all of the contained criteria match. + + - Example: + + ```toml + [[clients]] + match.all = [ + { sandbox-app-id = "com.spotify.Client" }, + { sandbox-engine = "org.flatpak" }, + ] + ``` + any: + kind: array + items: + ref: ClientMatch + required: false + description: | + Matches if any of the contained criteria match. + + - Example: + + ```toml + [[clients]] + match.any = [ + { sandbox-app-id = "com.spotify.Client" }, + { sandbox-app-id = "com.valvesoftware.Steam" }, + ] + ``` + exactly: + ref: ClientMatchExactly + required: false + description: | + Matches if a specific number of contained criteria match. + + - Example: + + ```toml + # Matches any client that is either steam or sandboxed by flatpak but not both. + [[clients]] + match.exactly.num = 1 + match.exactly.list = [ + { sandbox-engine = "org.flatpak" }, + { sandbox-app-id = "com.valvesoftware.Steam" }, + ] + ``` + + +ClientMatchExactly: + kind: table + description: | + Criterion for matching a specific number of client criteria. + fields: + num: + kind: number + required: true + description: The number of criteria that must match. + list: + kind: array + items: + ref: ClientMatch + required: true + description: The list of criteria. From 9bf79bf23cadc73ad2fcc67d8df83315cd7ed67b Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 2 May 2025 17:48:44 +0200 Subject: [PATCH 11/35] config: add sandbox client criteria --- jay-config/src/_private.rs | 7 +- jay-config/src/_private/client.rs | 14 ++-- jay-config/src/client.rs | 14 ++++ src/acceptor.rs | 9 ++- src/client.rs | 6 ++ src/config/handler.rs | 13 ++- src/criteria.rs | 1 - src/criteria/clm.rs | 54 ++++++++++--- src/criteria/clm/clm_matchers.rs | 4 +- .../clm/clm_matchers/clmm_sandboxed.rs | 14 ++++ src/criteria/clm/clm_matchers/clmm_string.rs | 79 +++++++++++++++++++ src/criteria/crit_matchers/critm_string.rs | 1 - src/security_context_acceptor.rs | 32 +++++--- src/xwayland.rs | 2 + toml-config/src/config.rs | 7 ++ .../src/config/parsers/client_match.rs | 48 +++++++++-- toml-config/src/rules.rs | 31 +++++++- toml-spec/spec/spec.generated.json | 28 +++++++ toml-spec/spec/spec.generated.md | 77 ++++++++++++++++++ toml-spec/spec/spec.yaml | 70 ++++++++++++++++ 20 files changed, 465 insertions(+), 46 deletions(-) create mode 100644 src/criteria/clm/clm_matchers/clmm_sandboxed.rs create mode 100644 src/criteria/clm/clm_matchers/clmm_string.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 32c28af9..95188583 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -81,7 +81,12 @@ pub enum ClientCriterionIpc { field: ClientCriterionStringField, regex: bool, }, + Sandboxed, } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] -pub enum ClientCriterionStringField {} +pub enum ClientCriterionStringField { + SandboxEngine, + SandboxAppId, + SandboxInstanceId, +} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index fd0208db..2a9d1639 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -3,8 +3,8 @@ use { crate::{ _private::{ - ClientCriterionIpc, Config, ConfigEntry, ConfigEntryGen, GenericCriterionIpc, - PollableId, VERSION, WireMode, bincode_ops, + ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen, + GenericCriterionIpc, PollableId, VERSION, WireMode, bincode_ops, ipc::{ ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, }, @@ -1501,7 +1501,6 @@ impl ConfigClient { criterion: ClientCriterion<'_>, child: bool, ) -> (ClientMatcher, bool) { - #[expect(unused_macros)] macro_rules! string { ($t:expr, $field:ident, $regex:expr) => { ClientCriterionIpc::String { @@ -1530,15 +1529,20 @@ impl ConfigClient { destroy_matcher, ) }; - #[expect(unused_variables)] let criterion = match criterion { ClientCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)), ClientCriterion::Not(c) => return generic(GenericCriterion::Not(c)), ClientCriterion::All(c) => return generic(GenericCriterion::All(c)), ClientCriterion::Any(c) => return generic(GenericCriterion::Any(c)), ClientCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)), + ClientCriterion::SandboxEngine(t) => string!(t, SandboxEngine, false), + ClientCriterion::SandboxEngineRegex(t) => string!(t, SandboxEngine, true), + ClientCriterion::SandboxAppId(t) => string!(t, SandboxAppId, false), + ClientCriterion::SandboxAppIdRegex(t) => string!(t, SandboxAppId, true), + ClientCriterion::SandboxInstanceId(t) => string!(t, SandboxInstanceId, false), + ClientCriterion::SandboxInstanceIdRegex(t) => string!(t, SandboxInstanceId, true), + ClientCriterion::Sandboxed => ClientCriterionIpc::Sandboxed, }; - #[expect(unreachable_code)] let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion }); get_response!( res, diff --git a/jay-config/src/client.rs b/jay-config/src/client.rs index e60e100f..601053f4 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -63,6 +63,20 @@ pub enum ClientCriterion<'a> { Any(&'a [ClientCriterion<'a>]), /// Matches if an exact number of the contained criteria match. Exactly(usize, &'a [ClientCriterion<'a>]), + /// Matches the engine name of the client's sandbox verbatim. + SandboxEngine(&'a str), + /// Matches the engine name of the client's sandbox with a regular expression. + SandboxEngineRegex(&'a str), + /// Matches the app id of the client's sandbox verbatim. + SandboxAppId(&'a str), + /// Matches the app id of the client's sandbox with a regular expression. + SandboxAppIdRegex(&'a str), + /// Matches the instance id of the client's sandbox verbatim. + SandboxInstanceId(&'a str), + /// Matches the instance id of the client's sandbox with a regular expression. + SandboxInstanceIdRegex(&'a str), + /// Matches if the client is sandboxed. + Sandboxed, } impl ClientCriterion<'_> { diff --git a/src/acceptor.rs b/src/acceptor.rs index e2c2cc70..41d0b468 100644 --- a/src/acceptor.rs +++ b/src/acceptor.rs @@ -2,6 +2,7 @@ use { crate::{ async_engine::SpawnedFuture, client::{CAPS_DEFAULT, ClientCaps}, + security_context_acceptor::AcceptorMetadata, state::State, utils::{errorfmt::ErrorFmt, oserror::OsError, xrd::xrd}, }, @@ -170,6 +171,7 @@ impl Acceptor { } async fn accept(fd: Rc, state: Rc, effective_caps: ClientCaps) { + let metadata = Rc::new(AcceptorMetadata::default()); loop { let fd = match state.ring.accept(&fd, c::SOCK_CLOEXEC).await { Ok(fd) => fd, @@ -179,9 +181,10 @@ async fn accept(fd: Rc, state: Rc, effective_caps: ClientCaps) { } }; let id = state.clients.id(); - if let Err(e) = state - .clients - .spawn(id, &state, fd, effective_caps, ClientCaps::all()) + if let Err(e) = + state + .clients + .spawn(id, &state, fd, effective_caps, ClientCaps::all(), &metadata) { log::error!("Could not spawn a client: {}", ErrorFmt(e)); break; diff --git a/src/client.rs b/src/client.rs index 42075314..5fdc960d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,6 +13,7 @@ use { }, leaks::Tracker, object::{Interface, Object, ObjectId, WL_DISPLAY_ID}, + security_context_acceptor::AcceptorMetadata, state::State, utils::{ activation_token::ActivationToken, @@ -125,6 +126,7 @@ impl Clients { socket: Rc, effective_caps: ClientCaps, bounding_caps: ClientCaps, + acceptor: &Rc, ) -> Result<(), ClientError> { let Some((uid, pid)) = get_socket_creds(&socket) else { return Ok(()); @@ -138,6 +140,7 @@ impl Clients { effective_caps, bounding_caps, false, + acceptor, )?; Ok(()) } @@ -152,6 +155,7 @@ impl Clients { effective_caps: ClientCaps, bounding_caps: ClientCaps, is_xwayland: bool, + acceptor: &Rc, ) -> Result, ClientError> { let data = Rc::new_cyclic(|slf| Client { id, @@ -183,6 +187,7 @@ impl Clients { focus_stealing_serial: Default::default(), changed_properties: Default::default(), destroyed: Default::default(), + acceptor: acceptor.clone(), }); track!(data, data); let display = Rc::new(WlDisplay::new(&data)); @@ -306,6 +311,7 @@ pub struct Client { pub focus_stealing_serial: Cell>, pub changed_properties: Cell, pub destroyed: CopyHashMap>>>, + pub acceptor: Rc, } pub const NUM_CACHED_SERIAL_RANGES: usize = 64; diff --git a/src/config/handler.rs b/src/config/handler.rs index ace6a277..82cb3454 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -41,7 +41,8 @@ use { bincode::Options, jay_config::{ _private::{ - ClientCriterionIpc, GenericCriterionIpc, PollableId, WireMode, bincode_ops, + ClientCriterionIpc, ClientCriterionStringField, GenericCriterionIpc, PollableId, + WireMode, bincode_ops, ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, @@ -1868,7 +1869,6 @@ impl ConfigProxyHandler { field, regex, } => { - #[expect(unused_variables)] let needle = match *regex { true => { let regex = Regex::new(string).map_err(CphError::InvalidRegex)?; @@ -1876,8 +1876,15 @@ impl ConfigProxyHandler { } false => CritLiteralOrRegex::Literal(string.to_string()), }; - match *field {} + match *field { + ClientCriterionStringField::SandboxEngine => mgr.sandbox_engine(needle), + ClientCriterionStringField::SandboxAppId => mgr.sandbox_app_id(needle), + ClientCriterionStringField::SandboxInstanceId => { + mgr.sandbox_instance_id(needle) + } + } } + ClientCriterionIpc::Sandboxed => mgr.sandboxed(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria.rs b/src/criteria.rs index 2da4f95b..b55827ae 100644 --- a/src/criteria.rs +++ b/src/criteria.rs @@ -84,7 +84,6 @@ pub trait CritMgrExt: CritMgr { upstream.not(self) } - #[expect(dead_code)] fn root(&self, criterion: T) -> Rc> where T: CritRootCriterion, diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 19538ef0..44e77ce7 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -4,16 +4,28 @@ use { crate::{ client::{Client, ClientId}, criteria::{ - CritDestroyListener, CritMatcherId, CritMatcherIds, CritUpstreamNode, FixedRootMatcher, - RootMatcherMap, - crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, + CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt, + CritUpstreamNode, FixedRootMatcher, RootMatcherMap, + clm::clm_matchers::{ + clmm_sandboxed::ClmMatchSandboxed, + clmm_string::{ + ClmMatchSandboxAppId, ClmMatchSandboxEngine, ClmMatchSandboxInstanceId, + }, + }, + crit_graph::{ + CritMgr, CritRoot, CritRootFixed, CritTarget, CritTargetOwner, WeakCritTargetOwner, + }, crit_leaf::{CritLeafEvent, CritLeafMatcher}, crit_matchers::critm_constant::CritMatchConstant, }, state::State, utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue}, }, - std::rc::{Rc, Weak}, + linearize::static_map, + std::{ + marker::PhantomData, + rc::{Rc, Weak}, + }, }; bitflags! { @@ -29,14 +41,18 @@ pub struct ClMatcherManager { changes: AsyncQueue>, leaf_events: Rc>>>, constant: ClmFixedRootMatcher>>, + sandboxed: ClmFixedRootMatcher, matchers: Rc, } -#[expect(dead_code)] type ClmRootMatcherMap = RootMatcherMap, T>; #[derive(Default)] -pub struct RootMatchers {} +pub struct RootMatchers { + sandbox_app_id: ClmRootMatcherMap, + sandbox_engine: ClmRootMatcherMap, + sandbox_instance_id: ClmRootMatcherMap, +} pub async fn handle_cl_changes(state: Rc) { let mgr = &state.cl_matcher_manager; @@ -56,14 +72,12 @@ pub async fn handle_cl_leaf_events(state: Rc) { } } -#[expect(dead_code)] pub type ClmUpstreamNode = dyn CritUpstreamNode>; pub type ClmLeafMatcher = CritLeafMatcher>; impl ClMatcherManager { pub fn new(ids: &Rc) -> Self { let matchers = Rc::new(RootMatchers::default()); - #[expect(unused_macros)] macro_rules! bool { ($name:ident) => {{ static_map! { @@ -77,6 +91,7 @@ impl ClMatcherManager { } Self { constant: CritMatchConstant::create(&matchers, ids), + sandboxed: bool!(ClmMatchSandboxed), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -109,7 +124,6 @@ impl ClMatcherManager { } return; } - #[expect(unused_macros)] macro_rules! handlers { ($name:ident) => { self.matchers @@ -119,7 +133,6 @@ impl ClMatcherManager { .filter_map(|m| m.upgrade()) }; } - #[expect(unused_macros)] macro_rules! fixed { ($name:ident) => { self.$name[false].handle(data); @@ -128,7 +141,6 @@ impl ClMatcherManager { } if changed.contains(CL_CHANGED_NEW) { changed |= ClMatcherChange::all(); - #[expect(unused_macros)] macro_rules! unconditional { ($field:ident) => { for m in handlers!($field) { @@ -136,9 +148,29 @@ impl ClMatcherManager { } }; } + unconditional!(sandbox_instance_id); + unconditional!(sandbox_app_id); + unconditional!(sandbox_engine); + fixed!(sandboxed); self.constant[true].handle(data); } } + + pub fn sandbox_engine(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchSandboxEngine::new(string)) + } + + pub fn sandbox_app_id(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchSandboxAppId::new(string)) + } + + pub fn sandbox_instance_id(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchSandboxInstanceId::new(string)) + } + + pub fn sandboxed(&self) -> Rc { + self.sandboxed[true].clone() + } } impl CritTarget for Rc { diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs index 246a4f9c..58d2968b 100644 --- a/src/criteria/clm/clm_matchers.rs +++ b/src/criteria/clm/clm_matchers.rs @@ -1,4 +1,3 @@ -#[expect(unused_macros)] macro_rules! fixed_root_criterion { ($ty:ty, $field:ident) => { impl crate::criteria::crit_graph::CritFixedRootCriterionBase> @@ -17,3 +16,6 @@ macro_rules! fixed_root_criterion { } }; } + +pub mod clmm_sandboxed; +pub mod clmm_string; diff --git a/src/criteria/clm/clm_matchers/clmm_sandboxed.rs b/src/criteria/clm/clm_matchers/clmm_sandboxed.rs new file mode 100644 index 00000000..4988e9b1 --- /dev/null +++ b/src/criteria/clm/clm_matchers/clmm_sandboxed.rs @@ -0,0 +1,14 @@ +use { + crate::{client::Client, criteria::crit_graph::CritFixedRootCriterion}, + std::rc::Rc, +}; + +pub struct ClmMatchSandboxed(pub bool); + +fixed_root_criterion!(ClmMatchSandboxed, sandboxed); + +impl CritFixedRootCriterion> for ClmMatchSandboxed { + fn matches(&self, data: &Rc) -> bool { + data.acceptor.sandboxed + } +} diff --git a/src/criteria/clm/clm_matchers/clmm_string.rs b/src/criteria/clm/clm_matchers/clmm_string.rs new file mode 100644 index 00000000..9e4c58c7 --- /dev/null +++ b/src/criteria/clm/clm_matchers/clmm_string.rs @@ -0,0 +1,79 @@ +use { + crate::{ + client::Client, + criteria::{ + clm::{ClmRootMatcherMap, RootMatchers}, + crit_matchers::critm_string::{CritMatchString, StringAccess}, + }, + security_context_acceptor::AcceptorMetadata, + }, + std::{marker::PhantomData, rc::Rc}, +}; + +pub type ClmMatchString = CritMatchString, T>; + +pub type ClmMatchSandboxEngine = ClmMatchString>; +pub type ClmMatchSandboxAppId = ClmMatchString>; +pub type ClmMatchSandboxInstanceId = ClmMatchString>; + +pub struct AcceptorMetadataAccess(PhantomData); + +trait SandboxField: Sized + 'static { + fn field(meta: &AcceptorMetadata) -> &Option; + fn nodes( + roots: &RootMatchers, + ) -> &ClmRootMatcherMap>>; +} + +pub struct SandboxEngineField; +pub struct SandboxAppIdField; +pub struct SandboxInstanceIdField; + +impl StringAccess> for AcceptorMetadataAccess +where + T: SandboxField, +{ + fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { + f(T::field(&data.acceptor).as_deref().unwrap_or_default()) + } + + fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap> { + T::nodes(roots) + } +} + +impl SandboxField for SandboxEngineField { + fn field(meta: &AcceptorMetadata) -> &Option { + &meta.sandbox_engine + } + + fn nodes( + roots: &RootMatchers, + ) -> &ClmRootMatcherMap>> { + &roots.sandbox_engine + } +} + +impl SandboxField for SandboxAppIdField { + fn field(meta: &AcceptorMetadata) -> &Option { + &meta.app_id + } + + fn nodes( + roots: &RootMatchers, + ) -> &ClmRootMatcherMap>> { + &roots.sandbox_app_id + } +} + +impl SandboxField for SandboxInstanceIdField { + fn field(meta: &AcceptorMetadata) -> &Option { + &meta.instance_id + } + + fn nodes( + roots: &RootMatchers, + ) -> &ClmRootMatcherMap>> { + &roots.sandbox_instance_id + } +} diff --git a/src/criteria/crit_matchers/critm_string.rs b/src/criteria/crit_matchers/critm_string.rs index b486ea0f..1464e2d6 100644 --- a/src/criteria/crit_matchers/critm_string.rs +++ b/src/criteria/crit_matchers/critm_string.rs @@ -22,7 +22,6 @@ where } impl CritMatchString { - #[expect(dead_code)] pub fn new(string: CritLiteralOrRegex) -> Self { Self { string, diff --git a/src/security_context_acceptor.rs b/src/security_context_acceptor.rs index 2a3e6141..dabdea37 100644 --- a/src/security_context_acceptor.rs +++ b/src/security_context_acceptor.rs @@ -24,9 +24,7 @@ linear_ids!(AcceptorIds, AcceptorId, u64); struct Acceptor { id: AcceptorId, state: Rc, - sandbox_engine: Option, - app_id: Option, - instance_id: Option, + metadata: Rc, listen_fd: Rc, close_fd: Rc, caps: ClientCaps, @@ -34,6 +32,14 @@ struct Acceptor { close_future: Cell>>, } +#[derive(Default)] +pub struct AcceptorMetadata { + pub sandboxed: bool, + pub sandbox_engine: Option, + pub app_id: Option, + pub instance_id: Option, +} + impl SecurityContextAcceptors { pub fn clear(&self) { for acceptor in self.acceptors.lock().drain_values() { @@ -54,9 +60,12 @@ impl SecurityContextAcceptors { let acceptor = Rc::new(Acceptor { id: self.ids.next(), state: state.clone(), - sandbox_engine, - app_id, - instance_id, + metadata: Rc::new(AcceptorMetadata { + sandboxed: true, + sandbox_engine, + app_id, + instance_id, + }), listen_fd: listen_fd.clone(), close_fd: close_fd.clone(), caps, @@ -100,7 +109,10 @@ impl Acceptor { } }; let id = s.clients.id(); - if let Err(e) = s.clients.spawn(id, s, fd, self.caps, self.caps) { + if let Err(e) = s + .clients + .spawn(id, s, fd, self.caps, self.caps, &self.metadata) + { log::error!("Could not spawn a client: {}", ErrorFmt(e)); break; } @@ -119,9 +131,9 @@ impl Display for Acceptor { write!( f, "{}/{}/{}", - self.sandbox_engine.as_deref().unwrap_or(""), - self.app_id.as_deref().unwrap_or(""), - self.instance_id.as_deref().unwrap_or(""), + self.metadata.sandbox_engine.as_deref().unwrap_or(""), + self.metadata.app_id.as_deref().unwrap_or(""), + self.metadata.instance_id.as_deref().unwrap_or(""), ) } } diff --git a/src/xwayland.rs b/src/xwayland.rs index ab078f50..6d6fb1e1 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -12,6 +12,7 @@ use { wl_surface::x_surface::xwindow::{Xwindow, XwindowData}, }, io_uring::IoUringError, + security_context_acceptor::AcceptorMetadata, state::State, user_session::import_environment, utils::{ @@ -179,6 +180,7 @@ async fn run( ClientCaps::all(), ClientCaps::all(), true, + &Rc::new(AcceptorMetadata::default()), ); let client = match client { Ok(c) => c, diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index d01cbc93..ad678a8c 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -225,6 +225,13 @@ pub struct ClientRule { #[derive(Default, Debug, Clone)] pub struct ClientMatch { pub generic: GenericMatch, + pub sandbox_engine: Option, + pub sandbox_engine_regex: Option, + pub sandbox_app_id: Option, + pub sandbox_app_id_regex: Option, + pub sandbox_instance_id: Option, + pub sandbox_instance_id_regex: Option, + pub sandboxed: 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 b3a1ca54..d82b5d9e 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -3,7 +3,7 @@ use { config::{ ClientMatch, GenericMatch, MatchExactly, context::Context, - extractor::{Extractor, ExtractorError, arr, n32, opt, str, val}, + extractor::{Extractor, ExtractorError, arr, bol, n32, opt, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, }, toml::{ @@ -36,13 +36,38 @@ impl Parser for ClientMatchParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let ((name, not_val, all_val, any_val, exactly_val),) = ext.extract((( - opt(str("name")), - opt(val("not")), - opt(arr("all")), - opt(arr("any")), - opt(val("exactly")), - ),))?; + let ( + ( + name, + not_val, + all_val, + any_val, + exactly_val, + sandboxed, + sandbox_engine, + sandbox_engine_regex, + sandbox_app_id, + sandbox_app_id_regex, + ), + (sandbox_instance_id, sandbox_instance_id_regex), + ) = ext.extract(( + ( + opt(str("name")), + opt(val("not")), + opt(arr("all")), + opt(arr("any")), + opt(val("exactly")), + opt(bol("sandboxed")), + opt(str("sandbox-engine")), + opt(str("sandbox-engine-regex")), + opt(str("sandbox-app-id")), + opt(str("sandbox-app-id-regex")), + ), + ( + opt(str("sandbox-instance-id")), + opt(str("sandbox-instance-id-regex")), + ), + ))?; let mut not = None; if let Some(value) = not_val { not = Some(Box::new(value.parse(&mut ClientMatchParser(self.0))?)); @@ -74,6 +99,13 @@ impl Parser for ClientMatchParser<'_> { any, exactly, }, + sandbox_engine: sandbox_engine.despan_into(), + sandbox_engine_regex: sandbox_engine_regex.despan_into(), + sandbox_app_id: sandbox_app_id.despan_into(), + sandbox_app_id_regex: sandbox_app_id_regex.despan_into(), + sandbox_instance_id: sandbox_instance_id.despan_into(), + sandbox_instance_id_regex: sandbox_instance_id_regex.despan_into(), + sandboxed: sandboxed.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 87eff524..73cab65f 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -84,9 +84,36 @@ impl Rule for ClientRule { fn map_custom( _state: &Rc, - _all: &mut Vec>, - _match_: &Self::Match, + all: &mut Vec>, + match_: &Self::Match, ) -> Option<()> { + let m = |c: ClientCriterion<'_>| MatcherTemp(c.to_matcher()); + macro_rules! value_ref { + ($ty:ident, $field:ident) => { + if let Some(value) = &match_.$field { + all.push(m(ClientCriterion::$ty(value))); + } + }; + } + macro_rules! bool { + ($ty:ident, $field:ident) => { + if let Some(value) = &match_.$field { + let crit = ClientCriterion::$ty; + let matcher = match value { + false => m(ClientCriterion::Not(&crit)), + true => m(crit), + }; + all.push(matcher); + } + }; + } + value_ref!(SandboxEngine, sandbox_engine); + value_ref!(SandboxEngineRegex, sandbox_engine_regex); + value_ref!(SandboxAppId, sandbox_app_id); + value_ref!(SandboxAppIdRegex, sandbox_app_id_regex); + value_ref!(SandboxInstanceId, sandbox_instance_id); + value_ref!(SandboxInstanceIdRegex, sandbox_instance_id_regex); + bool!(Sandboxed, sandboxed); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index eef59221..1c65d462 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -531,6 +531,34 @@ "exactly": { "description": "Matches if a specific number of contained criteria match.\n\n- Example:\n\n ```toml\n # Matches any client that is either steam or sandboxed by flatpak but not both.\n [[clients]]\n match.exactly.num = 1\n match.exactly.list = [\n { sandbox-engine = \"org.flatpak\" },\n { sandbox-app-id = \"com.valvesoftware.Steam\" },\n ]\n ```\n", "$ref": "#/$defs/ClientMatchExactly" + }, + "sandboxed": { + "type": "boolean", + "description": "Matches if the client is/isn't sandboxed.\n\n- Example:\n\n ```toml\n [[clients]]\n match.sandboxed = true\n ```\n" + }, + "sandbox-engine": { + "type": "string", + "description": "Matches the engine name of the client's sandbox verbatim.\n\n- Example:\n\n ```toml\n [[clients]]\n match.sandbox-engine = \"org.flatpak\"\n ```\n" + }, + "sandbox-engine-regex": { + "type": "string", + "description": "Matches the engine name of the client's sandbox with a regular expression.\n\n- Example:\n\n ```toml\n [[clients]]\n match.sandbox-engine = \"flatpak\"\n ```\n" + }, + "sandbox-app-id": { + "type": "string", + "description": "Matches the app id of the client's sandbox verbatim.\n\n- Example:\n\n ```toml\n [[clients]]\n match.sandbox-app-id = \"com.spotify.Client\"\n ```\n" + }, + "sandbox-app-id-regex": { + "type": "string", + "description": "Matches the app id of the client's sandbox with a regular expression.\n\n- Example:\n\n ```toml\n [[clients]]\n match.sandbox-app-id-regex = \"(?i)spotify\"\n ```\n" + }, + "sandbox-instance-id": { + "type": "string", + "description": "Matches the instance id of the client's sandbox verbatim.\n" + }, + "sandbox-instance-id-regex": { + "type": "string", + "description": "Matches the instance id of the client's sandbox with a regular expression.\n" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 1c5b44a0..b7a23796 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -794,6 +794,83 @@ The table has the following fields: The value of this field should be a [ClientMatchExactly](#types-ClientMatchExactly). +- `sandboxed` (optional): + + Matches if the client is/isn't sandboxed. + + - Example: + + ```toml + [[clients]] + match.sandboxed = true + ``` + + The value of this field should be a boolean. + +- `sandbox-engine` (optional): + + Matches the engine name of the client's sandbox verbatim. + + - Example: + + ```toml + [[clients]] + match.sandbox-engine = "org.flatpak" + ``` + + The value of this field should be a string. + +- `sandbox-engine-regex` (optional): + + Matches the engine name of the client's sandbox with a regular expression. + + - Example: + + ```toml + [[clients]] + match.sandbox-engine = "flatpak" + ``` + + The value of this field should be a string. + +- `sandbox-app-id` (optional): + + Matches the app id of the client's sandbox verbatim. + + - Example: + + ```toml + [[clients]] + match.sandbox-app-id = "com.spotify.Client" + ``` + + The value of this field should be a string. + +- `sandbox-app-id-regex` (optional): + + Matches the app id of the client's sandbox with a regular expression. + + - Example: + + ```toml + [[clients]] + match.sandbox-app-id-regex = "(?i)spotify" + ``` + + The value of this field should be a string. + +- `sandbox-instance-id` (optional): + + Matches the instance id of the client's sandbox verbatim. + + The value of this field should be a string. + +- `sandbox-instance-id-regex` (optional): + + Matches the instance id of the client's sandbox 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 e44eac62..510292f3 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3167,6 +3167,76 @@ ClientMatch: { sandbox-app-id = "com.valvesoftware.Steam" }, ] ``` + sandboxed: + kind: boolean + required: false + description: | + Matches if the client is/isn't sandboxed. + + - Example: + + ```toml + [[clients]] + match.sandboxed = true + ``` + sandbox-engine: + kind: string + required: false + description: | + Matches the engine name of the client's sandbox verbatim. + + - Example: + + ```toml + [[clients]] + match.sandbox-engine = "org.flatpak" + ``` + sandbox-engine-regex: + kind: string + required: false + description: | + Matches the engine name of the client's sandbox with a regular expression. + + - Example: + + ```toml + [[clients]] + match.sandbox-engine = "flatpak" + ``` + sandbox-app-id: + kind: string + required: false + description: | + Matches the app id of the client's sandbox verbatim. + + - Example: + + ```toml + [[clients]] + match.sandbox-app-id = "com.spotify.Client" + ``` + sandbox-app-id-regex: + kind: string + required: false + description: | + Matches the app id of the client's sandbox with a regular expression. + + - Example: + + ```toml + [[clients]] + match.sandbox-app-id-regex = "(?i)spotify" + ``` + sandbox-instance-id: + kind: string + required: false + description: | + Matches the instance id of the client's sandbox verbatim. + sandbox-instance-id-regex: + kind: string + required: false + description: | + Matches the instance id of the client's sandbox with a regular expression. ClientMatchExactly: From 587ffc7ee5c442afa22c3ff30acd4503b40ef17c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 12:48:44 +0200 Subject: [PATCH 12/35] config: add uid client criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/client.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/clm.rs | 7 +++++++ src/criteria/clm/clm_matchers.rs | 1 + src/criteria/clm/clm_matchers/clmm_uid.rs | 20 +++++++++++++++++++ toml-config/src/config.rs | 1 + .../src/config/parsers/client_match.rs | 6 ++++-- toml-config/src/rules.rs | 8 ++++++++ toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 8 ++++++++ toml-spec/spec/spec.yaml | 5 +++++ 13 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/criteria/clm/clm_matchers/clmm_uid.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 95188583..29359e2f 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -82,6 +82,7 @@ pub enum ClientCriterionIpc { regex: bool, }, Sandboxed, + Uid(i32), } #[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 2a9d1639..12c8e3b4 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1542,6 +1542,7 @@ impl ConfigClient { ClientCriterion::SandboxInstanceId(t) => string!(t, SandboxInstanceId, false), ClientCriterion::SandboxInstanceIdRegex(t) => string!(t, SandboxInstanceId, true), ClientCriterion::Sandboxed => ClientCriterionIpc::Sandboxed, + ClientCriterion::Uid(p) => ClientCriterionIpc::Uid(p), }; 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 601053f4..1b2d4206 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -77,6 +77,8 @@ pub enum ClientCriterion<'a> { SandboxInstanceIdRegex(&'a str), /// Matches if the client is sandboxed. Sandboxed, + /// Matches the user ID of the client. + Uid(i32), } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 82cb3454..b34500a6 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1885,6 +1885,7 @@ impl ConfigProxyHandler { } } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), + ClientCriterionIpc::Uid(p) => mgr.uid(*p), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 44e77ce7..a1d67a68 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -11,6 +11,7 @@ use { clmm_string::{ ClmMatchSandboxAppId, ClmMatchSandboxEngine, ClmMatchSandboxInstanceId, }, + clmm_uid::ClmMatchUid, }, crit_graph::{ CritMgr, CritRoot, CritRootFixed, CritTarget, CritTargetOwner, WeakCritTargetOwner, @@ -52,6 +53,7 @@ pub struct RootMatchers { sandbox_app_id: ClmRootMatcherMap, sandbox_engine: ClmRootMatcherMap, sandbox_instance_id: ClmRootMatcherMap, + uid: ClmRootMatcherMap, } pub async fn handle_cl_changes(state: Rc) { @@ -151,6 +153,7 @@ impl ClMatcherManager { unconditional!(sandbox_instance_id); unconditional!(sandbox_app_id); unconditional!(sandbox_engine); + unconditional!(uid); fixed!(sandboxed); self.constant[true].handle(data); } @@ -171,6 +174,10 @@ impl ClMatcherManager { pub fn sandboxed(&self) -> Rc { self.sandboxed[true].clone() } + + pub fn uid(&self, pid: i32) -> Rc { + self.root(ClmMatchUid(pid as _)) + } } impl CritTarget for Rc { diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs index 58d2968b..b7886e61 100644 --- a/src/criteria/clm/clm_matchers.rs +++ b/src/criteria/clm/clm_matchers.rs @@ -19,3 +19,4 @@ macro_rules! fixed_root_criterion { pub mod clmm_sandboxed; pub mod clmm_string; +pub mod clmm_uid; diff --git a/src/criteria/clm/clm_matchers/clmm_uid.rs b/src/criteria/clm/clm_matchers/clmm_uid.rs new file mode 100644 index 00000000..6056b955 --- /dev/null +++ b/src/criteria/clm/clm_matchers/clmm_uid.rs @@ -0,0 +1,20 @@ +use { + crate::{ + client::Client, + criteria::{RootMatcherMap, clm::RootMatchers, crit_graph::CritRootCriterion}, + }, + std::rc::Rc, + uapi::c, +}; + +pub struct ClmMatchUid(pub c::uid_t); + +impl CritRootCriterion> for ClmMatchUid { + fn matches(&self, data: &Rc) -> bool { + data.pid_info.uid == self.0 + } + + fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap, Self>> { + Some(&roots.uid) + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index ad678a8c..c5c53eff 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -232,6 +232,7 @@ pub struct ClientMatch { pub sandbox_instance_id: Option, pub sandbox_instance_id_regex: Option, pub sandboxed: Option, + pub uid: 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 d82b5d9e..59c8a22f 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -3,7 +3,7 @@ use { config::{ ClientMatch, GenericMatch, MatchExactly, context::Context, - extractor::{Extractor, ExtractorError, arr, bol, n32, opt, str, val}, + extractor::{Extractor, ExtractorError, arr, bol, n32, opt, s32, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, }, toml::{ @@ -49,7 +49,7 @@ impl Parser for ClientMatchParser<'_> { sandbox_app_id, sandbox_app_id_regex, ), - (sandbox_instance_id, sandbox_instance_id_regex), + (sandbox_instance_id, sandbox_instance_id_regex, uid), ) = ext.extract(( ( opt(str("name")), @@ -66,6 +66,7 @@ impl Parser for ClientMatchParser<'_> { ( opt(str("sandbox-instance-id")), opt(str("sandbox-instance-id-regex")), + opt(s32("uid")), ), ))?; let mut not = None; @@ -106,6 +107,7 @@ impl Parser for ClientMatchParser<'_> { sandbox_instance_id: sandbox_instance_id.despan_into(), sandbox_instance_id_regex: sandbox_instance_id_regex.despan_into(), sandboxed: sandboxed.despan(), + uid: uid.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 73cab65f..f077d928 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -95,6 +95,13 @@ impl Rule for ClientRule { } }; } + macro_rules! value { + ($ty:ident, $field:ident) => { + if let Some(value) = match_.$field { + all.push(m(ClientCriterion::$ty(value))); + } + }; + } macro_rules! bool { ($ty:ident, $field:ident) => { if let Some(value) = &match_.$field { @@ -113,6 +120,7 @@ impl Rule for ClientRule { value_ref!(SandboxAppIdRegex, sandbox_app_id_regex); value_ref!(SandboxInstanceId, sandbox_instance_id); value_ref!(SandboxInstanceIdRegex, sandbox_instance_id_regex); + value!(Uid, uid); bool!(Sandboxed, sandboxed); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1c65d462..b343ca76 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -559,6 +559,10 @@ "sandbox-instance-id-regex": { "type": "string", "description": "Matches the instance id of the client's sandbox with a regular expression.\n" + }, + "uid": { + "type": "integer", + "description": "Matches the user ID of the client." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index b7a23796..f7011d77 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -871,6 +871,14 @@ The table has the following fields: The value of this field should be a string. +- `uid` (optional): + + Matches the user ID of the client. + + The value of this field should be a number. + + The numbers should be integers. + ### `ClientMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 510292f3..37cdb901 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3237,6 +3237,11 @@ ClientMatch: required: false description: | Matches the instance id of the client's sandbox with a regular expression. + uid: + kind: number + integer_only: true + required: false + description: Matches the user ID of the client. ClientMatchExactly: From a952e658dae4ed167ad396a8dcdc7c5bcb83b2b9 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 12:46:56 +0200 Subject: [PATCH 13/35] config: add pid client criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/client.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/clm.rs | 7 +++++++ src/criteria/clm/clm_matchers.rs | 1 + src/criteria/clm/clm_matchers/clmm_pid.rs | 20 +++++++++++++++++++ toml-config/src/config.rs | 1 + .../src/config/parsers/client_match.rs | 4 +++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 8 ++++++++ toml-spec/spec/spec.yaml | 5 +++++ 13 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/criteria/clm/clm_matchers/clmm_pid.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 29359e2f..1a5a5b62 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -83,6 +83,7 @@ pub enum ClientCriterionIpc { }, Sandboxed, Uid(i32), + Pid(i32), } #[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 12c8e3b4..5deb5c9a 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1543,6 +1543,7 @@ impl ConfigClient { ClientCriterion::SandboxInstanceIdRegex(t) => string!(t, SandboxInstanceId, true), ClientCriterion::Sandboxed => ClientCriterionIpc::Sandboxed, ClientCriterion::Uid(p) => ClientCriterionIpc::Uid(p), + ClientCriterion::Pid(p) => ClientCriterionIpc::Pid(p), }; 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 1b2d4206..d8c5a547 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -79,6 +79,8 @@ pub enum ClientCriterion<'a> { Sandboxed, /// Matches the user ID of the client. Uid(i32), + /// Matches the process ID of the client. + Pid(i32), } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index b34500a6..c513894b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1886,6 +1886,7 @@ impl ConfigProxyHandler { } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), ClientCriterionIpc::Uid(p) => mgr.uid(*p), + ClientCriterionIpc::Pid(p) => mgr.pid(*p), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index a1d67a68..7027e703 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -7,6 +7,7 @@ use { CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, clm::clm_matchers::{ + clmm_pid::ClmMatchPid, clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ ClmMatchSandboxAppId, ClmMatchSandboxEngine, ClmMatchSandboxInstanceId, @@ -54,6 +55,7 @@ pub struct RootMatchers { sandbox_engine: ClmRootMatcherMap, sandbox_instance_id: ClmRootMatcherMap, uid: ClmRootMatcherMap, + pid: ClmRootMatcherMap, } pub async fn handle_cl_changes(state: Rc) { @@ -154,6 +156,7 @@ impl ClMatcherManager { unconditional!(sandbox_app_id); unconditional!(sandbox_engine); unconditional!(uid); + unconditional!(pid); fixed!(sandboxed); self.constant[true].handle(data); } @@ -178,6 +181,10 @@ impl ClMatcherManager { pub fn uid(&self, pid: i32) -> Rc { self.root(ClmMatchUid(pid as _)) } + + pub fn pid(&self, pid: i32) -> Rc { + self.root(ClmMatchPid(pid as _)) + } } impl CritTarget for Rc { diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs index b7886e61..422a44f4 100644 --- a/src/criteria/clm/clm_matchers.rs +++ b/src/criteria/clm/clm_matchers.rs @@ -17,6 +17,7 @@ macro_rules! fixed_root_criterion { }; } +pub mod clmm_pid; pub mod clmm_sandboxed; pub mod clmm_string; pub mod clmm_uid; diff --git a/src/criteria/clm/clm_matchers/clmm_pid.rs b/src/criteria/clm/clm_matchers/clmm_pid.rs new file mode 100644 index 00000000..fc7ae8dc --- /dev/null +++ b/src/criteria/clm/clm_matchers/clmm_pid.rs @@ -0,0 +1,20 @@ +use { + crate::{ + client::Client, + criteria::{RootMatcherMap, clm::RootMatchers, crit_graph::CritRootCriterion}, + }, + std::rc::Rc, + uapi::c, +}; + +pub struct ClmMatchPid(pub c::pid_t); + +impl CritRootCriterion> for ClmMatchPid { + fn matches(&self, data: &Rc) -> bool { + data.pid_info.pid == self.0 + } + + fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap, Self>> { + Some(&roots.pid) + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index c5c53eff..381b24f4 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -233,6 +233,7 @@ pub struct ClientMatch { pub sandbox_instance_id_regex: Option, pub sandboxed: Option, pub uid: Option, + pub pid: 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 59c8a22f..a6076b46 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -49,7 +49,7 @@ impl Parser for ClientMatchParser<'_> { sandbox_app_id, sandbox_app_id_regex, ), - (sandbox_instance_id, sandbox_instance_id_regex, uid), + (sandbox_instance_id, sandbox_instance_id_regex, uid, pid), ) = ext.extract(( ( opt(str("name")), @@ -67,6 +67,7 @@ impl Parser for ClientMatchParser<'_> { opt(str("sandbox-instance-id")), opt(str("sandbox-instance-id-regex")), opt(s32("uid")), + opt(s32("pid")), ), ))?; let mut not = None; @@ -108,6 +109,7 @@ impl Parser for ClientMatchParser<'_> { sandbox_instance_id_regex: sandbox_instance_id_regex.despan_into(), sandboxed: sandboxed.despan(), uid: uid.despan(), + pid: pid.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index f077d928..e5ce8978 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -121,6 +121,7 @@ impl Rule for ClientRule { value_ref!(SandboxInstanceId, sandbox_instance_id); value_ref!(SandboxInstanceIdRegex, sandbox_instance_id_regex); value!(Uid, uid); + value!(Pid, pid); bool!(Sandboxed, sandboxed); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index b343ca76..af20bc5f 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -563,6 +563,10 @@ "uid": { "type": "integer", "description": "Matches the user ID of the client." + }, + "pid": { + "type": "integer", + "description": "Matches the process ID of the client." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index f7011d77..19878aaf 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -879,6 +879,14 @@ The table has the following fields: The numbers should be integers. +- `pid` (optional): + + Matches the process ID of the client. + + The value of this field should be a number. + + The numbers should be integers. + ### `ClientMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 37cdb901..fc0ae7c7 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3242,6 +3242,11 @@ ClientMatch: integer_only: true required: false description: Matches the user ID of the client. + pid: + kind: number + integer_only: true + required: false + description: Matches the process ID of the client. ClientMatchExactly: From bdabb7bbddef2ba5a91cfba31c8b1d566cb680e2 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 12:55:22 +0200 Subject: [PATCH 14/35] config: add xwayland client criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/client.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/clm.rs | 8 ++++++++ src/criteria/clm/clm_matchers.rs | 1 + src/criteria/clm/clm_matchers/clmm_is_xwayland.rs | 14 ++++++++++++++ toml-config/src/config.rs | 1 + toml-config/src/config/parsers/client_match.rs | 4 +++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 6 ++++++ toml-spec/spec/spec.yaml | 4 ++++ 13 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/criteria/clm/clm_matchers/clmm_is_xwayland.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 1a5a5b62..ba22f5a8 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -84,6 +84,7 @@ pub enum ClientCriterionIpc { Sandboxed, Uid(i32), Pid(i32), + IsXwayland, } #[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 5deb5c9a..93993232 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1544,6 +1544,7 @@ impl ConfigClient { ClientCriterion::Sandboxed => ClientCriterionIpc::Sandboxed, ClientCriterion::Uid(p) => ClientCriterionIpc::Uid(p), ClientCriterion::Pid(p) => ClientCriterionIpc::Pid(p), + ClientCriterion::IsXwayland => ClientCriterionIpc::IsXwayland, }; 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 d8c5a547..9fed07ea 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -81,6 +81,8 @@ pub enum ClientCriterion<'a> { Uid(i32), /// Matches the process ID of the client. Pid(i32), + /// Matches if the client is Xwayland. + IsXwayland, } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index c513894b..b329947e 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1887,6 +1887,7 @@ impl ConfigProxyHandler { ClientCriterionIpc::Sandboxed => mgr.sandboxed(), ClientCriterionIpc::Uid(p) => mgr.uid(*p), ClientCriterionIpc::Pid(p) => mgr.pid(*p), + ClientCriterionIpc::IsXwayland => mgr.is_xwayland(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 7027e703..243ae514 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -7,6 +7,7 @@ use { CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, clm::clm_matchers::{ + clmm_is_xwayland::ClmMatchIsXwayland, clmm_pid::ClmMatchPid, clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ @@ -44,6 +45,7 @@ pub struct ClMatcherManager { leaf_events: Rc>>>, constant: ClmFixedRootMatcher>>, sandboxed: ClmFixedRootMatcher, + is_xwayland: ClmFixedRootMatcher, matchers: Rc, } @@ -96,6 +98,7 @@ impl ClMatcherManager { Self { constant: CritMatchConstant::create(&matchers, ids), sandboxed: bool!(ClmMatchSandboxed), + is_xwayland: bool!(ClmMatchIsXwayland), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -158,6 +161,7 @@ impl ClMatcherManager { unconditional!(uid); unconditional!(pid); fixed!(sandboxed); + fixed!(is_xwayland); self.constant[true].handle(data); } } @@ -185,6 +189,10 @@ impl ClMatcherManager { pub fn pid(&self, pid: i32) -> Rc { self.root(ClmMatchPid(pid as _)) } + + pub fn is_xwayland(&self) -> Rc { + self.is_xwayland[true].clone() + } } impl CritTarget for Rc { diff --git a/src/criteria/clm/clm_matchers.rs b/src/criteria/clm/clm_matchers.rs index 422a44f4..bd661aa4 100644 --- a/src/criteria/clm/clm_matchers.rs +++ b/src/criteria/clm/clm_matchers.rs @@ -17,6 +17,7 @@ macro_rules! fixed_root_criterion { }; } +pub mod clmm_is_xwayland; pub mod clmm_pid; pub mod clmm_sandboxed; pub mod clmm_string; diff --git a/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs b/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs new file mode 100644 index 00000000..4f71c47f --- /dev/null +++ b/src/criteria/clm/clm_matchers/clmm_is_xwayland.rs @@ -0,0 +1,14 @@ +use { + crate::{client::Client, criteria::crit_graph::CritFixedRootCriterion}, + std::rc::Rc, +}; + +pub struct ClmMatchIsXwayland(pub bool); + +fixed_root_criterion!(ClmMatchIsXwayland, is_xwayland); + +impl CritFixedRootCriterion> for ClmMatchIsXwayland { + fn matches(&self, data: &Rc) -> bool { + data.is_xwayland + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 381b24f4..207adfc9 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -234,6 +234,7 @@ pub struct ClientMatch { pub sandboxed: Option, pub uid: Option, pub pid: Option, + pub is_xwayland: 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 a6076b46..31be8bbb 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -49,7 +49,7 @@ impl Parser for ClientMatchParser<'_> { sandbox_app_id, sandbox_app_id_regex, ), - (sandbox_instance_id, sandbox_instance_id_regex, uid, pid), + (sandbox_instance_id, sandbox_instance_id_regex, uid, pid, is_xwayland), ) = ext.extract(( ( opt(str("name")), @@ -68,6 +68,7 @@ impl Parser for ClientMatchParser<'_> { opt(str("sandbox-instance-id-regex")), opt(s32("uid")), opt(s32("pid")), + opt(bol("is-xwayland")), ), ))?; let mut not = None; @@ -110,6 +111,7 @@ impl Parser for ClientMatchParser<'_> { sandboxed: sandboxed.despan(), uid: uid.despan(), pid: pid.despan(), + is_xwayland: is_xwayland.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index e5ce8978..9f482fdb 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -123,6 +123,7 @@ impl Rule for ClientRule { value!(Uid, uid); value!(Pid, pid); bool!(Sandboxed, sandboxed); + bool!(IsXwayland, is_xwayland); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index af20bc5f..3ec70136 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -567,6 +567,10 @@ "pid": { "type": "integer", "description": "Matches the process ID of the client." + }, + "is-xwayland": { + "type": "boolean", + "description": "Matches if the client is/isn't Xwayland." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 19878aaf..a20505dd 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -887,6 +887,12 @@ The table has the following fields: The numbers should be integers. +- `is-xwayland` (optional): + + Matches if the client is/isn't Xwayland. + + The value of this field should be a boolean. + ### `ClientMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index fc0ae7c7..1c93dd78 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3247,6 +3247,10 @@ ClientMatch: integer_only: true required: false description: Matches the process ID of the client. + is-xwayland: + kind: boolean + required: false + description: Matches if the client is/isn't Xwayland. ClientMatchExactly: From cc734a135c0eee7d9817035d0764eec918ef19e3 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 13:03:48 +0200 Subject: [PATCH 15/35] config: add comm client criteria --- 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 | 12 ++++++++++++ toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/client_match.rs | 14 +++++++++++++- 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 ++++++++ 12 files changed, 73 insertions(+), 2 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index ba22f5a8..7229991c 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -92,4 +92,5 @@ pub enum ClientCriterionStringField { SandboxEngine, SandboxAppId, SandboxInstanceId, + Comm, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 93993232..559bec40 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1545,6 +1545,8 @@ impl ConfigClient { ClientCriterion::Uid(p) => ClientCriterionIpc::Uid(p), ClientCriterion::Pid(p) => ClientCriterionIpc::Pid(p), ClientCriterion::IsXwayland => ClientCriterionIpc::IsXwayland, + ClientCriterion::Comm(t) => string!(t, Comm, false), + ClientCriterion::CommRegex(t) => string!(t, Comm, 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 9fed07ea..6dc7487b 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -83,6 +83,10 @@ pub enum ClientCriterion<'a> { Pid(i32), /// Matches if the client is Xwayland. IsXwayland, + /// Matches the `/proc/pid/comm` of the client verbatim. + Comm(&'a str), + /// Matches the `/proc/pid/comm` of the client with a regular expression. + CommRegex(&'a str), } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index b329947e..00a9fecd 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1882,6 +1882,7 @@ impl ConfigProxyHandler { ClientCriterionStringField::SandboxInstanceId => { mgr.sandbox_instance_id(needle) } + ClientCriterionStringField::Comm => mgr.comm(needle), } } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 243ae514..802d26f8 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -11,7 +11,8 @@ use { clmm_pid::ClmMatchPid, clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ - ClmMatchSandboxAppId, ClmMatchSandboxEngine, ClmMatchSandboxInstanceId, + ClmMatchComm, ClmMatchSandboxAppId, ClmMatchSandboxEngine, + ClmMatchSandboxInstanceId, }, clmm_uid::ClmMatchUid, }, @@ -58,6 +59,7 @@ pub struct RootMatchers { sandbox_instance_id: ClmRootMatcherMap, uid: ClmRootMatcherMap, pid: ClmRootMatcherMap, + comm: ClmRootMatcherMap, } pub async fn handle_cl_changes(state: Rc) { @@ -160,6 +162,7 @@ impl ClMatcherManager { unconditional!(sandbox_engine); unconditional!(uid); unconditional!(pid); + unconditional!(comm); fixed!(sandboxed); fixed!(is_xwayland); self.constant[true].handle(data); @@ -193,6 +196,10 @@ impl ClMatcherManager { pub fn is_xwayland(&self) -> Rc { self.is_xwayland[true].clone() } + + pub fn comm(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchComm::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 9e4c58c7..c0369b32 100644 --- a/src/criteria/clm/clm_matchers/clmm_string.rs +++ b/src/criteria/clm/clm_matchers/clmm_string.rs @@ -15,8 +15,10 @@ pub type ClmMatchString = CritMatchString, T>; pub type ClmMatchSandboxEngine = ClmMatchString>; pub type ClmMatchSandboxAppId = ClmMatchString>; pub type ClmMatchSandboxInstanceId = ClmMatchString>; +pub type ClmMatchComm = ClmMatchString; pub struct AcceptorMetadataAccess(PhantomData); +pub struct CommAccess; trait SandboxField: Sized + 'static { fn field(meta: &AcceptorMetadata) -> &Option; @@ -77,3 +79,13 @@ impl SandboxField for SandboxInstanceIdField { &roots.sandbox_instance_id } } + +impl StringAccess> for CommAccess { + fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { + f(&data.pid_info.comm) + } + + fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap> { + &roots.comm + } +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 207adfc9..8f51e220 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -235,6 +235,8 @@ pub struct ClientMatch { pub uid: Option, pub pid: Option, pub is_xwayland: Option, + pub comm: Option, + pub comm_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 31be8bbb..852cd9d2 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -49,7 +49,15 @@ impl Parser for ClientMatchParser<'_> { sandbox_app_id, sandbox_app_id_regex, ), - (sandbox_instance_id, sandbox_instance_id_regex, uid, pid, is_xwayland), + ( + sandbox_instance_id, + sandbox_instance_id_regex, + uid, + pid, + is_xwayland, + comm, + comm_regex, + ), ) = ext.extract(( ( opt(str("name")), @@ -69,6 +77,8 @@ impl Parser for ClientMatchParser<'_> { opt(s32("uid")), opt(s32("pid")), opt(bol("is-xwayland")), + opt(str("comm")), + opt(str("comm-regex")), ), ))?; let mut not = None; @@ -112,6 +122,8 @@ impl Parser for ClientMatchParser<'_> { uid: uid.despan(), pid: pid.despan(), is_xwayland: is_xwayland.despan(), + comm: comm.despan_into(), + comm_regex: comm_regex.despan_into(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 9f482fdb..c54550ec 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -120,6 +120,8 @@ impl Rule for ClientRule { value_ref!(SandboxAppIdRegex, sandbox_app_id_regex); value_ref!(SandboxInstanceId, sandbox_instance_id); value_ref!(SandboxInstanceIdRegex, sandbox_instance_id_regex); + value_ref!(Comm, comm); + value_ref!(CommRegex, comm_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 3ec70136..92dcdaa8 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -571,6 +571,14 @@ "is-xwayland": { "type": "boolean", "description": "Matches if the client is/isn't Xwayland." + }, + "comm": { + "type": "string", + "description": "Matches the `/proc/pid/comm` of the client verbatim." + }, + "comm-regex": { + "type": "string", + "description": "Matches the `/proc/pid/comm` 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 a20505dd..c7286091 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -893,6 +893,18 @@ The table has the following fields: The value of this field should be a boolean. +- `comm` (optional): + + Matches the `/proc/pid/comm` of the client verbatim. + + The value of this field should be a string. + +- `comm-regex` (optional): + + Matches the `/proc/pid/comm` 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 1c93dd78..f124139b 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3251,6 +3251,14 @@ ClientMatch: kind: boolean required: false description: Matches if the client is/isn't Xwayland. + comm: + kind: string + required: false + description: Matches the `/proc/pid/comm` of the client verbatim. + comm-regex: + kind: string + required: false + description: Matches the `/proc/pid/comm` of the client with a regular expression. ClientMatchExactly: From a6257910bb534762813bbb80fc0d660dfd7acdaf Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 13:09:13 +0200 Subject: [PATCH 16/35] config: add exe client criteria --- 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 | 8 ++++++- src/criteria/clm/clm_matchers/clmm_string.rs | 12 +++++++++++ src/utils/pid_info.rs | 21 ++++++++++++++++++- toml-config/src/config.rs | 2 ++ .../src/config/parsers/client_match.rs | 6 ++++++ 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, 85 insertions(+), 2 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 7229991c..6b75daa1 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -93,4 +93,5 @@ pub enum ClientCriterionStringField { SandboxAppId, SandboxInstanceId, Comm, + Exe, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 559bec40..df02a491 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1547,6 +1547,8 @@ impl ConfigClient { ClientCriterion::IsXwayland => ClientCriterionIpc::IsXwayland, ClientCriterion::Comm(t) => string!(t, Comm, false), ClientCriterion::CommRegex(t) => string!(t, Comm, true), + ClientCriterion::Exe(t) => string!(t, Exe, false), + ClientCriterion::ExeRegex(t) => string!(t, Exe, 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 6dc7487b..97b8ff39 100644 --- a/jay-config/src/client.rs +++ b/jay-config/src/client.rs @@ -87,6 +87,10 @@ pub enum ClientCriterion<'a> { Comm(&'a str), /// Matches the `/proc/pid/comm` of the client with a regular expression. CommRegex(&'a str), + /// Matches the `/proc/pid/exe` of the client verbatim. + Exe(&'a str), + /// Matches the `/proc/pid/exe` of the client with a regular expression. + ExeRegex(&'a str), } impl ClientCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 00a9fecd..6758eb01 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1883,6 +1883,7 @@ impl ConfigProxyHandler { mgr.sandbox_instance_id(needle) } ClientCriterionStringField::Comm => mgr.comm(needle), + ClientCriterionStringField::Exe => mgr.exe(needle), } } ClientCriterionIpc::Sandboxed => mgr.sandboxed(), diff --git a/src/criteria/clm.rs b/src/criteria/clm.rs index 802d26f8..57a929cf 100644 --- a/src/criteria/clm.rs +++ b/src/criteria/clm.rs @@ -11,7 +11,7 @@ use { clmm_pid::ClmMatchPid, clmm_sandboxed::ClmMatchSandboxed, clmm_string::{ - ClmMatchComm, ClmMatchSandboxAppId, ClmMatchSandboxEngine, + ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine, ClmMatchSandboxInstanceId, }, clmm_uid::ClmMatchUid, @@ -60,6 +60,7 @@ pub struct RootMatchers { uid: ClmRootMatcherMap, pid: ClmRootMatcherMap, comm: ClmRootMatcherMap, + exe: ClmRootMatcherMap, } pub async fn handle_cl_changes(state: Rc) { @@ -163,6 +164,7 @@ impl ClMatcherManager { unconditional!(uid); unconditional!(pid); unconditional!(comm); + unconditional!(exe); fixed!(sandboxed); fixed!(is_xwayland); self.constant[true].handle(data); @@ -200,6 +202,10 @@ impl ClMatcherManager { pub fn comm(&self, string: CritLiteralOrRegex) -> Rc { self.root(ClmMatchComm::new(string)) } + + pub fn exe(&self, string: CritLiteralOrRegex) -> Rc { + self.root(ClmMatchExe::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 c0369b32..626c5f3d 100644 --- a/src/criteria/clm/clm_matchers/clmm_string.rs +++ b/src/criteria/clm/clm_matchers/clmm_string.rs @@ -16,9 +16,11 @@ pub type ClmMatchSandboxEngine = ClmMatchString>; pub type ClmMatchSandboxInstanceId = ClmMatchString>; pub type ClmMatchComm = ClmMatchString; +pub type ClmMatchExe = ClmMatchString; pub struct AcceptorMetadataAccess(PhantomData); pub struct CommAccess; +pub struct ExeAccess; trait SandboxField: Sized + 'static { fn field(meta: &AcceptorMetadata) -> &Option; @@ -89,3 +91,13 @@ impl StringAccess> for CommAccess { &roots.comm } } + +impl StringAccess> for ExeAccess { + fn with_string(data: &Rc, f: impl FnOnce(&str) -> bool) -> bool { + f(&data.pid_info.exe) + } + + fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap> { + &roots.exe + } +} diff --git a/src/utils/pid_info.rs b/src/utils/pid_info.rs index d2c672c4..aec37028 100644 --- a/src/utils/pid_info.rs +++ b/src/utils/pid_info.rs @@ -1,6 +1,7 @@ use { crate::utils::{errorfmt::ErrorFmt, oserror::OsError}, bstr::ByteSlice, + std::os::unix::ffi::OsStrExt, uapi::{OwnedFd, c}, }; @@ -8,6 +9,7 @@ pub struct PidInfo { pub uid: c::uid_t, pub pid: c::pid_t, pub comm: String, + pub exe: String, } pub fn get_pid_info(uid: c::uid_t, pid: c::pid_t) -> PidInfo { @@ -18,7 +20,24 @@ pub fn get_pid_info(uid: c::uid_t, pid: c::pid_t) -> PidInfo { "Unknown".to_string() } }; - PidInfo { uid, pid, comm } + let exe = match std::fs::read_link(format!("/proc/{}/exe", pid)) { + Ok(name) => name + .as_os_str() + .as_bytes() + .trim_ascii_end() + .as_bstr() + .to_string(), + Err(e) => { + log::warn!("Could not read `exe` of pid {}: {}", pid, ErrorFmt(e)); + "Unknown".to_string() + } + }; + PidInfo { + uid, + pid, + comm, + exe, + } } pub fn get_socket_creds(socket: &OwnedFd) -> Option<(c::uid_t, c::pid_t)> { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 8f51e220..a19fb0a3 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -237,6 +237,8 @@ pub struct ClientMatch { pub is_xwayland: Option, pub comm: Option, pub comm_regex: Option, + pub exe: Option, + pub exe_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 852cd9d2..013e7646 100644 --- a/toml-config/src/config/parsers/client_match.rs +++ b/toml-config/src/config/parsers/client_match.rs @@ -57,6 +57,8 @@ impl Parser for ClientMatchParser<'_> { is_xwayland, comm, comm_regex, + exe, + exe_regex, ), ) = ext.extract(( ( @@ -79,6 +81,8 @@ impl Parser for ClientMatchParser<'_> { opt(bol("is-xwayland")), opt(str("comm")), opt(str("comm-regex")), + opt(str("exe")), + opt(str("exe-regex")), ), ))?; let mut not = None; @@ -124,6 +128,8 @@ impl Parser for ClientMatchParser<'_> { is_xwayland: is_xwayland.despan(), comm: comm.despan_into(), comm_regex: comm_regex.despan_into(), + exe: exe.despan_into(), + exe_regex: exe_regex.despan_into(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index c54550ec..468fdb41 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -122,6 +122,8 @@ impl Rule for ClientRule { value_ref!(SandboxInstanceIdRegex, sandbox_instance_id_regex); value_ref!(Comm, comm); value_ref!(CommRegex, comm_regex); + value_ref!(Exe, exe); + value_ref!(ExeRegex, exe_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 92dcdaa8..cbd4ed2c 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -579,6 +579,14 @@ "comm-regex": { "type": "string", "description": "Matches the `/proc/pid/comm` of the client with a regular expression." + }, + "exe": { + "type": "string", + "description": "Matches the `/proc/pid/exe` of the client verbatim." + }, + "exe-regex": { + "type": "string", + "description": "Matches the `/proc/pid/exe` 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 c7286091..cdf4d275 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -905,6 +905,18 @@ The table has the following fields: The value of this field should be a string. +- `exe` (optional): + + Matches the `/proc/pid/exe` of the client verbatim. + + The value of this field should be a string. + +- `exe-regex` (optional): + + Matches the `/proc/pid/exe` 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 f124139b..2b3659e5 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3259,6 +3259,14 @@ ClientMatch: kind: string required: false description: Matches the `/proc/pid/comm` of the client with a regular expression. + exe: + kind: string + required: false + description: Matches the `/proc/pid/exe` of the client verbatim. + exe-regex: + kind: string + required: false + description: Matches the `/proc/pid/exe` of the client with a regular expression. ClientMatchExactly: From 59f8acdfde8d49b5d44a6f70fdd463058d8e273b Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 17:49:21 +0200 Subject: [PATCH 17/35] config: add window-rule infrastructure --- jay-config/src/_private.rs | 20 +- jay-config/src/_private/client.rs | 124 +++++++- jay-config/src/_private/ipc.rs | 24 +- jay-config/src/window.rs | 85 ++++++ src/compositor.rs | 7 + src/config.rs | 6 + src/config/handler.rs | 128 ++++++++- src/criteria.rs | 1 + src/criteria/crit_graph/crit_root.rs | 1 - src/criteria/tlm.rs | 271 ++++++++++++++++++ src/criteria/tlm/tlm_matchers.rs | 21 ++ src/criteria/tlm/tlm_matchers/tlmm_kind.rs | 31 ++ src/it/test_config.rs | 2 + src/state.rs | 4 +- src/tree/toplevel.rs | 29 +- toml-config/src/config.rs | 16 ++ toml-config/src/config/parsers.rs | 3 + toml-config/src/config/parsers/config.rs | 19 +- .../src/config/parsers/window_match.rs | 113 ++++++++ toml-config/src/config/parsers/window_rule.rs | 104 +++++++ toml-config/src/config/parsers/window_type.rs | 53 ++++ toml-config/src/lib.rs | 67 +++-- toml-config/src/rules.rs | 132 ++++++++- toml-spec/spec/spec.generated.json | 119 +++++++- toml-spec/spec/spec.generated.md | 263 ++++++++++++++++- toml-spec/spec/spec.yaml | 224 ++++++++++++++- 26 files changed, 1829 insertions(+), 38 deletions(-) create mode 100644 src/criteria/tlm.rs create mode 100644 src/criteria/tlm/tlm_matchers.rs create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_kind.rs create mode 100644 toml-config/src/config/parsers/window_match.rs create mode 100644 toml-config/src/config/parsers/window_rule.rs create mode 100644 toml-config/src/config/parsers/window_type.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 6b75daa1..2aa0c2ac 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -4,7 +4,11 @@ mod logging; pub(crate) mod string_error; use { - crate::{client::ClientMatcher, video::Mode}, + crate::{ + client::ClientMatcher, + video::Mode, + window::{WindowMatcher, WindowType}, + }, bincode::Options, serde::{Deserialize, Serialize}, std::marker::PhantomData, @@ -95,3 +99,17 @@ pub enum ClientCriterionStringField { Comm, Exe, } + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum WindowCriterionIpc { + Generic(GenericCriterionIpc), + String { + string: String, + field: WindowCriterionStringField, + regex: bool, + }, + Types(WindowType), +} + +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] +pub enum WindowCriterionStringField {} diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index df02a491..a1a7bc4c 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -4,7 +4,7 @@ use { crate::{ _private::{ ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen, - GenericCriterionIpc, PollableId, VERSION, WireMode, bincode_ops, + GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, WireMode, bincode_ops, ipc::{ ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource, }, @@ -31,7 +31,7 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, - window::{Window, WindowType}, + window::{MatchedWindow, Window, WindowCriterion, WindowMatcher, WindowType}, xwayland::XScalingMode, }, bincode::Options, @@ -114,6 +114,7 @@ pub(crate) struct ConfigClient { i3bar_separator: RefCell>>, pressed_keysym: Cell>, client_match_handlers: RefCell>, + window_match_handlers: RefCell>, feat_mod_mask: Cell, } @@ -123,6 +124,11 @@ struct ClientMatchHandler { latched: HashMap>, } +struct WindowMatchHandler { + cb: Callback, + latched: HashMap>, +} + struct Interest { result: Option>, waker: Option, @@ -253,6 +259,7 @@ pub unsafe extern "C" fn init( i3bar_separator: Default::default(), pressed_keysym: Cell::new(None), client_match_handlers: Default::default(), + window_match_handlers: Default::default(), feat_mod_mask: Cell::new(false), }); let init = unsafe { slice::from_raw_parts(init, size) }; @@ -1593,6 +1600,95 @@ impl ConfigClient { self.client_match_handlers.borrow_mut().remove(&matcher); } + pub fn create_window_matcher(&self, criterion: WindowCriterion) -> WindowMatcher { + self.create_window_matcher_(criterion, false).0 + } + + fn create_window_matcher_( + &self, + criterion: WindowCriterion, + child: bool, + ) -> (WindowMatcher, bool) { + #[expect(unused_macros)] + macro_rules! string { + ($t:expr, $field:ident, $regex:expr) => { + WindowCriterionIpc::String { + string: $t.to_string(), + field: WindowCriterionStringField::$field, + regex: $regex, + } + }; + } + let create_matcher = |criterion| { + let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { + criterion: WindowCriterionIpc::Generic(criterion), + }); + get_response!(res, WindowMatcher(0), CreateWindowMatcher { matcher }); + matcher + }; + let destroy_matcher = |matcher| { + self.send(&ClientMessage::DestroyWindowMatcher { matcher }); + }; + let generic = |crit: GenericCriterion| { + self.create_generic_matcher( + crit, + child, + |c| self.create_window_matcher_(c, true), + create_matcher, + destroy_matcher, + ) + }; + let criterion = match criterion { + WindowCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)), + WindowCriterion::Not(c) => return generic(GenericCriterion::Not(c)), + WindowCriterion::All(c) => return generic(GenericCriterion::All(c)), + WindowCriterion::Any(c) => return generic(GenericCriterion::Any(c)), + WindowCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)), + WindowCriterion::Types(t) => WindowCriterionIpc::Types(t), + }; + let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion }); + get_response!( + res, + (WindowMatcher(0), false), + CreateWindowMatcher { matcher } + ); + (matcher, true) + } + + pub fn set_window_matcher_handler( + &self, + matcher: WindowMatcher, + cb: impl FnMut(MatchedWindow) + 'static, + ) { + let cb = Rc::new(RefCell::new(cb)); + let handlers = &mut *self.window_match_handlers.borrow_mut(); + let handler = handlers.entry(matcher).or_insert_with(|| { + self.send(&ClientMessage::EnableWindowMatcherEvents { matcher }); + WindowMatchHandler { + cb: cb.clone(), + latched: Default::default(), + } + }); + handler.cb = cb.clone(); + } + + pub fn set_window_matcher_latch_handler( + &self, + matcher: WindowMatcher, + window: Window, + cb: impl FnOnce() + 'static, + ) { + let handlers = &mut *self.window_match_handlers.borrow_mut(); + if let Some(handler) = handlers.get_mut(&matcher) { + handler.latched.insert(window, Box::new(cb)); + } + } + + pub fn destroy_window_matcher(&self, matcher: WindowMatcher) { + self.send(&ClientMessage::DestroyWindowMatcher { matcher }); + self.window_match_handlers.borrow_mut().remove(&matcher); + } + fn handle_msg(&self, msg: &[u8]) { self.handle_msg2(msg); self.dispatch_futures(); @@ -1879,6 +1975,30 @@ impl ConfigClient { }; cb(); } + ServerMessage::WindowMatcherMatched { matcher, window } => { + let cb = { + let handlers = self.window_match_handlers.borrow(); + let Some(handler) = handlers.get(&matcher) else { + return; + }; + handler.cb.clone() + }; + let matched = MatchedWindow { matcher, window }; + cb.borrow_mut()(matched); + } + ServerMessage::WindowMatcherUnmatched { matcher, window } => { + let cb = { + let mut handlers = self.window_match_handlers.borrow_mut(); + let Some(handler) = handlers.get_mut(&matcher) else { + return; + }; + let Some(cb) = handler.latched.remove(&window) else { + return; + }; + cb + }; + cb(); + } } } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 3f9525ac..384c13ae 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -1,6 +1,6 @@ use { crate::{ - _private::{ClientCriterionIpc, PollableId, WireMode}, + _private::{ClientCriterionIpc, PollableId, WindowCriterionIpc, WireMode}, Axis, Direction, PciId, Workspace, client::{Client, ClientMatcher}, input::{ @@ -15,7 +15,7 @@ use { ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode, connector_type::ConnectorType, }, - window::{Window, WindowType}, + window::{Window, WindowMatcher, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -102,6 +102,14 @@ pub enum ServerMessage { matcher: ClientMatcher, client: Client, }, + WindowMatcherMatched { + matcher: WindowMatcher, + window: Window, + }, + WindowMatcherUnmatched { + matcher: WindowMatcher, + window: Window, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -681,6 +689,15 @@ pub enum ClientMessage<'a> { EnableClientMatcherEvents { matcher: ClientMatcher, }, + CreateWindowMatcher { + criterion: WindowCriterionIpc, + }, + DestroyWindowMatcher { + matcher: WindowMatcher, + }, + EnableWindowMatcherEvents { + matcher: WindowMatcher, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -904,6 +921,9 @@ pub enum Response { CreateClientMatcher { matcher: ClientMatcher, }, + CreateWindowMatcher { + matcher: WindowMatcher, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index ebf35122..3205e36c 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -3,6 +3,7 @@ use { crate::{Axis, Direction, Workspace, client::Client}, serde::{Deserialize, Serialize}, + std::ops::Deref, }; /// A toplevel window. @@ -202,3 +203,87 @@ impl Window { self.set_float_pinned(!self.float_pinned()); } } + +/// A window matcher. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct WindowMatcher(pub u64); + +/// A matched window. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct MatchedWindow { + pub(crate) matcher: WindowMatcher, + pub(crate) window: Window, +} + +/// A criterion for matching a window. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[non_exhaustive] +pub enum WindowCriterion<'a> { + /// Matches if the contained matcher matches. + Matcher(WindowMatcher), + /// Matches if the contained criterion does not match. + Not(&'a WindowCriterion<'a>), + /// Matches if the window has one of the types. + Types(WindowType), + /// Matches if all of the contained criteria match. + All(&'a [WindowCriterion<'a>]), + /// Matches if any of the contained criteria match. + Any(&'a [WindowCriterion<'a>]), + /// Matches if an exact number of the contained criteria match. + Exactly(usize, &'a [WindowCriterion<'a>]), +} + +impl WindowCriterion<'_> { + /// Converts the criterion to a matcher. + pub fn to_matcher(self) -> WindowMatcher { + get!(WindowMatcher(0)).create_window_matcher(self) + } + + /// Binds a function to execute when the criterion matches a window. + /// + /// This leaks the matcher. + pub fn bind(self, cb: F) { + self.to_matcher().bind(cb); + } +} + +impl WindowMatcher { + /// Destroys the matcher. + /// + /// Any bound callback will no longer be executed. + pub fn destroy(self) { + get!().destroy_window_matcher(self); + } + + /// Sets a function to execute when the criterion matches a window. + /// + /// Replaces any already bound callback. + pub fn bind(self, cb: F) { + get!().set_window_matcher_handler(self, cb); + } +} + +impl MatchedWindow { + /// Returns the window that matched. + pub fn window(self) -> Window { + self.window + } + + /// Returns the matcher. + pub fn matcher(self) -> WindowMatcher { + self.matcher + } + + /// Latches a function to be executed when the window no longer matches the criteria. + pub fn latch(self, cb: F) { + get!().set_window_matcher_latch_handler(self.matcher, self.window, cb); + } +} + +impl Deref for MatchedWindow { + type Target = Window; + + fn deref(&self) -> &Self::Target { + &self.window + } +} diff --git a/src/compositor.rs b/src/compositor.rs index b4854958..1a2627d1 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -18,6 +18,7 @@ use { criteria::{ CritMatcherIds, clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events}, + tlm::{TlMatcherManager, handle_tl_changes, handle_tl_leaf_events}, }, damage::{DamageVisualizer, visualize_damage}, dbus::Dbus, @@ -299,6 +300,7 @@ fn start_compositor2( icons: Default::default(), show_pin_icon: Cell::new(false), cl_matcher_manager: ClMatcherManager::new(&crit_ids), + tl_matcher_manager: TlMatcherManager::new(&crit_ids), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -476,6 +478,11 @@ fn start_global_event_handlers( "cl matcher leaf events", handle_cl_leaf_events(state.clone()), ), + eng.spawn("tl matcher manager", handle_tl_changes(state.clone())), + eng.spawn( + "tl matcher leaf events", + handle_tl_leaf_events(state.clone()), + ), ] } diff --git a/src/config.rs b/src/config.rs index ba31e844..841e3fad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,7 @@ use { input::{InputDevice, Seat, SwitchEvent}, keyboard::{mods::Modifiers, syms::KeySym}, video::{Connector, DrmDevice}, + window, }, libloading::Library, std::{cell::Cell, io, mem, ptr, rc::Rc}, @@ -218,6 +219,11 @@ impl ConfigProxy { client_matchers: Default::default(), client_matcher_cache: Default::default(), client_matcher_leafs: Default::default(), + window_matcher_ids: NumCell::new(1), + window_matchers: Default::default(), + window_matcher_cache: Default::default(), + window_matcher_leafs: Default::default(), + window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 6758eb01..b2adf0e9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -10,7 +10,9 @@ use { compositor::MAX_EXTENTS, config::ConfigProxy, criteria::{ - CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, clm::ClmLeafMatcher, + CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, + clm::ClmLeafMatcher, + tlm::{TlmLeafMatcher, TlmUpstreamNode}, }, format::config_formats, ifs::wl_seat::{SeatId, WlSeatGlobal}, @@ -22,9 +24,9 @@ use { theme::{Color, ThemeSized}, tree::{ ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode, - TearingMode, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig, move_ws_to_output, - toplevel_create_split, toplevel_parent_container, toplevel_set_floating, - toplevel_set_workspace, + TearingMode, ToplevelData, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig, + move_ws_to_output, toplevel_create_split, toplevel_parent_container, + toplevel_set_floating, toplevel_set_workspace, }, utils::{ asyncevent::AsyncEvent, @@ -42,7 +44,7 @@ use { jay_config::{ _private::{ ClientCriterionIpc, ClientCriterionStringField, GenericCriterionIpc, PollableId, - WireMode, bincode_ops, + WindowCriterionIpc, WireMode, bincode_ops, ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource}, }, Axis, Direction, Workspace, @@ -64,7 +66,7 @@ use { TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, - window::Window, + window::{Window, WindowMatcher}, xwayland::XScalingMode, }, libloading::Library, @@ -115,6 +117,13 @@ pub(super) struct ConfigProxyHandler { CopyHashMap>>>, pub client_matcher_cache: CriterionCache>, pub client_matcher_leafs: CopyHashMap>, + + pub window_matcher_ids: NumCell, + pub window_matchers: + CopyHashMap>>, + pub window_matcher_cache: CriterionCache, + pub window_matcher_leafs: CopyHashMap>, + pub window_matcher_std_kinds: Rc, } pub struct Pollable { @@ -159,7 +168,6 @@ where K: Hash + Eq, T: CritTarget, { - #[allow(clippy::allow_attributes, dead_code)] fn any(&self, v: &impl Fn(&K) -> bool) -> bool { v(&self.crit) || self.upstream.iter().any(|u| u.any(v)) } @@ -177,6 +185,9 @@ impl ConfigProxyHandler { self.client_matcher_leafs.clear(); self.client_matchers.clear(); + self.window_matcher_leafs.clear(); + self.window_matchers.clear(); + if let Some(path) = &self.path { if let Err(e) = uapi::unlink(path.as_str()) { log::error!("Could not unlink {}: {}", path, ErrorFmt(OsError(e.0))); @@ -1933,6 +1944,98 @@ impl ConfigProxyHandler { Ok(()) } + fn get_window_matcher( + &self, + matcher: WindowMatcher, + ) -> Result>, CphError> { + self.window_matchers + .get(&matcher) + .ok_or(CphError::WindowMatcherDoesNotExist(matcher)) + } + + fn handle_create_window_matcher( + &self, + mut criterion: WindowCriterionIpc, + ) -> Result<(), CphError> { + if let WindowCriterionIpc::Generic(generic) = &mut criterion { + self.sort_generic_matcher(generic, |m| m.0); + } + let id = WindowMatcher(self.window_matcher_ids.fetch_add(1)); + let cache = &self.window_matcher_cache; + if let Some(matcher) = cache.get(&criterion) { + if let Some(matcher) = matcher.upgrade() { + self.window_matchers.set(id, matcher); + self.respond(Response::CreateWindowMatcher { matcher: id }); + return Ok(()); + } + } + let mgr = &self.state.tl_matcher_manager; + let mut upstream = vec![]; + let matcher = match &criterion { + WindowCriterionIpc::Generic(m) => { + self.create_generic_matcher(mgr, m, &mut upstream, |m| self.get_window_matcher(*m))? + } + WindowCriterionIpc::String { + string, + field, + regex, + } => { + #[expect(unused_variables)] + let needle = match *regex { + true => { + let regex = Regex::new(string).map_err(CphError::InvalidRegex)?; + CritLiteralOrRegex::Regex(regex) + } + false => CritLiteralOrRegex::Literal(string.to_string()), + }; + match *field {} + } + WindowCriterionIpc::Types(t) => mgr.kind(*t), + }; + let cached = Rc::new(CachedCriterion { + crit: criterion.clone(), + cache: cache.clone(), + upstream, + node: matcher.clone(), + }); + cache.set(criterion, Rc::downgrade(&cached)); + self.window_matchers.set(id, cached); + self.respond(Response::CreateWindowMatcher { matcher: id }); + Ok(()) + } + + fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) { + self.window_matchers.remove(&matcher); + self.window_matcher_leafs.remove(&matcher); + } + + fn handle_enable_window_matcher_events( + self: &Rc, + matcher: WindowMatcher, + ) -> Result<(), CphError> { + if self.window_matcher_leafs.contains(&matcher) { + return Ok(()); + } + let upstream = self.get_window_matcher(matcher)?; + let mut node = upstream.node.clone(); + if !upstream.any(&|crit| matches!(crit, WindowCriterionIpc::Types(_))) { + let list = [self.window_matcher_std_kinds.clone(), node]; + node = self.state.tl_matcher_manager.list(&list, true); + } + let slf = self.clone(); + let leaf = self.state.tl_matcher_manager.leaf(&node, move |tl| { + let window = slf.tl_id_to_window(tl); + slf.send(&ServerMessage::WindowMatcherMatched { matcher, window }); + let slf = slf.clone(); + Box::new(move || { + slf.send(&ServerMessage::WindowMatcherUnmatched { matcher, window }); + }) + }); + self.window_matcher_leafs.set(matcher, leaf); + self.state.tl_matcher_manager.rematch_all(&self.state); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2729,6 +2832,15 @@ impl ConfigProxyHandler { ClientMessage::EnableClientMatcherEvents { matcher } => self .handle_enable_client_matcher_events(matcher) .wrn("enable_window_matcher_events")?, + ClientMessage::CreateWindowMatcher { criterion } => self + .handle_create_window_matcher(criterion) + .wrn("create_window_matcher")?, + ClientMessage::DestroyWindowMatcher { matcher } => { + self.handle_destroy_window_matcher(matcher) + } + ClientMessage::EnableWindowMatcherEvents { matcher } => self + .handle_enable_window_matcher_events(matcher) + .wrn("enable_window_matcher_events")?, } Ok(()) } @@ -2814,6 +2926,8 @@ enum CphError { ClientMatcherDoesNotExist(ClientMatcher), #[error("Could not parse regex")] InvalidRegex(#[source] regex::Error), + #[error("Window matcher {0:?} does not exist")] + WindowMatcherDoesNotExist(WindowMatcher), } trait WithRequestName { diff --git a/src/criteria.rs b/src/criteria.rs index b55827ae..cf8125f2 100644 --- a/src/criteria.rs +++ b/src/criteria.rs @@ -3,6 +3,7 @@ mod crit_graph; pub mod crit_leaf; mod crit_matchers; mod crit_per_target_data; +pub mod tlm; use { crate::{ diff --git a/src/criteria/crit_graph/crit_root.rs b/src/criteria/crit_graph/crit_root.rs index 9992f40d..c8ca8abd 100644 --- a/src/criteria/crit_graph/crit_root.rs +++ b/src/criteria/crit_graph/crit_root.rs @@ -141,7 +141,6 @@ where self.downstream.update_matched(target, node, new, !new); } - #[expect(dead_code)] pub fn has_downstream(&self) -> bool { self.downstream.has_downstream() } diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs new file mode 100644 index 00000000..376af00f --- /dev/null +++ b/src/criteria/tlm.rs @@ -0,0 +1,271 @@ +pub mod tlm_matchers; + +use { + crate::{ + criteria::{ + CritDestroyListener, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, + FixedRootMatcher, RootMatcherMap, + crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, + crit_leaf::{CritLeafEvent, CritLeafMatcher}, + crit_matchers::critm_constant::CritMatchConstant, + tlm::tlm_matchers::tlmm_kind::TlmMatchKind, + }, + state::State, + tree::{NodeId, ToplevelData, ToplevelNode}, + utils::{ + copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue, + toplevel_identifier::ToplevelIdentifier, + }, + }, + jay_config::window::WindowType, + std::rc::{Rc, Weak}, +}; + +bitflags! { + TlMatcherChange: u32; + TL_CHANGED_DESTROYED = 1 << 0, + TL_CHANGED_NEW = 1 << 1, +} + +type TlmFixedRootMatcher = FixedRootMatcher; + +pub struct TlMatcherManager { + ids: Rc, + changes: AsyncQueue>, + leaf_events: Rc>>, + constant: TlmFixedRootMatcher>, + matchers: Rc, +} + +type TlmRootMatcherMap = RootMatcherMap; + +#[derive(Default)] +pub struct RootMatchers { + kinds: TlmRootMatcherMap, +} + +pub async fn handle_tl_changes(state: Rc) { + let mgr = &state.tl_matcher_manager; + loop { + let tl = mgr.changes.pop().await; + mgr.update_matches(tl); + } +} + +pub async fn handle_tl_leaf_events(state: Rc) { + let mgr = &state.tl_matcher_manager; + let debouncer = state.ring.debouncer(1000); + loop { + let event = mgr.leaf_events.pop().await; + event.run(); + debouncer.debounce().await; + } +} + +pub type TlmUpstreamNode = dyn CritUpstreamNode; +pub type TlmLeafMatcher = CritLeafMatcher; + +impl TlMatcherManager { + pub fn new(ids: &Rc) -> Self { + let matchers = Rc::new(RootMatchers::default()); + Self { + constant: CritMatchConstant::create(&matchers, ids), + changes: Default::default(), + leaf_events: Default::default(), + ids: ids.clone(), + matchers, + } + } + + pub fn clear(&self) { + self.changes.clear(); + self.leaf_events.clear(); + } + + pub fn rematch_all(&self, state: &Rc) { + for tl in state.toplevels.lock().values() { + if let Some(tl) = tl.upgrade() { + tl.tl_data().property_changed(TL_CHANGED_NEW); + } + } + } + + pub fn has_no_interest(&self, data: &ToplevelData, change: TlMatcherChange) -> bool { + !self.has_interest(data, change) + } + + pub fn has_interest(&self, data: &ToplevelData, mut change: TlMatcherChange) -> bool { + if change.contains(TL_CHANGED_DESTROYED) && data.destroyed.is_not_empty() { + return true; + } + #[expect(unused_macros)] + macro_rules! fixed { + ($name:ident) => { + if self.$name[false].has_downstream() || self.$name[true].has_downstream() { + return true; + } + }; + } + if change.contains(TL_CHANGED_NEW) { + macro_rules! unconditional { + ($field:ident) => { + if self.matchers.$field.is_not_empty() { + return true; + } + }; + } + unconditional!(kinds); + if self.constant[true].has_downstream() { + return true; + } + change |= TlMatcherChange::all(); + } + #[expect(unused_macros)] + macro_rules! conditional { + ($change:expr, $field:ident) => { + if change.contains($change) && self.matchers.$field.is_not_empty() { + return true; + } + }; + } + #[expect(unused_macros)] + macro_rules! fixed_conditional { + ($change:expr, $field:ident) => { + if change.contains($change) { + fixed!($field); + } + }; + } + false + } + + pub fn changed(&self, node: Rc) { + self.changes.push(node); + } + + fn update_matches(&self, node: Rc) { + let data = node.tl_data(); + let mut changed = data.changed_properties.replace(TlMatcherChange::none()); + if changed.contains(TL_CHANGED_DESTROYED) { + for destroyed in data.destroyed.lock().drain_values() { + if let Some(destroyed) = destroyed.upgrade() { + destroyed.destroyed(data.node_id); + } + } + } + if data.parent.is_none() { + return; + } + macro_rules! handlers { + ($name:ident) => { + self.matchers + .$name + .lock() + .values() + .filter_map(|m| m.upgrade()) + }; + } + #[expect(unused_macros)] + macro_rules! fixed { + ($name:ident) => { + self.$name[false].handle(data); + self.$name[true].handle(data); + }; + } + if changed.contains(TL_CHANGED_NEW) { + changed |= TlMatcherChange::all(); + macro_rules! unconditional { + ($field:ident) => { + for m in handlers!($field) { + m.handle(data); + } + }; + } + unconditional!(kinds); + self.constant[true].handle(data); + } + #[expect(unused_macros)] + macro_rules! conditional { + ($change:expr, $field:ident) => { + if changed.contains($change) { + for m in handlers!($field) { + m.handle(data); + } + } + }; + } + #[expect(unused_macros)] + macro_rules! fixed_conditional { + ($change:expr, $field:ident) => { + if changed.contains($change) { + fixed!($field); + } + }; + } + } + + pub fn kind(&self, kind: WindowType) -> Rc { + self.root(TlmMatchKind::new(kind)) + } +} + +impl CritTarget for ToplevelData { + type Id = NodeId; + type Mgr = TlMatcherManager; + type RootMatchers = RootMatchers; + type LeafData = ToplevelIdentifier; + type Owner = Weak; + + fn owner(&self) -> Self::Owner { + self.slf.clone() + } + + fn id(&self) -> Self::Id { + self.node_id + } + + fn destroyed(&self) -> &CopyHashMap>> { + &self.destroyed + } + + fn leaf_data(&self) -> Self::LeafData { + self.identifier.get() + } +} + +impl CritTargetOwner for Rc { + type Target = ToplevelData; + + fn data(&self) -> &Self::Target { + self.tl_data() + } +} + +impl WeakCritTargetOwner for Weak { + type Target = ToplevelData; + type Owner = Rc; + + fn upgrade(&self) -> Option { + self.upgrade() + } +} + +impl CritMgr for TlMatcherManager { + type Target = ToplevelData; + + fn id(&self) -> CritMatcherId { + self.ids.next() + } + + fn leaf_events(&self) -> &Rc>> { + &self.leaf_events + } + + fn match_constant(&self) -> &FixedRootMatcher> { + &self.constant + } + + fn roots(&self) -> &Rc<::RootMatchers> { + &self.matchers + } +} diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs new file mode 100644 index 00000000..93871e79 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers.rs @@ -0,0 +1,21 @@ +#[expect(unused_macros)] +macro_rules! fixed_root_criterion { + ($ty:ty, $field:ident) => { + impl crate::criteria::crit_graph::CritFixedRootCriterionBase + for $ty + { + fn constant(&self) -> bool { + self.0 + } + + fn not<'a>( + &self, + mgr: &'a crate::criteria::tlm::TlMatcherManager, + ) -> &'a crate::criteria::FixedRootMatcher { + &mgr.$field + } + } + }; +} + +pub mod tlmm_kind; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_kind.rs b/src/criteria/tlm/tlm_matchers/tlmm_kind.rs new file mode 100644 index 00000000..8c332877 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_kind.rs @@ -0,0 +1,31 @@ +use { + crate::{ + criteria::{ + crit_graph::CritRootCriterion, + tlm::{RootMatchers, TlmRootMatcherMap}, + }, + tree::ToplevelData, + utils::bitflags::BitflagsExt, + }, + jay_config::window::WindowType, +}; + +pub struct TlmMatchKind { + kind: WindowType, +} + +impl TlmMatchKind { + pub fn new(kind: WindowType) -> TlmMatchKind { + Self { kind } + } +} + +impl CritRootCriterion for TlmMatchKind { + fn matches(&self, data: &ToplevelData) -> bool { + self.kind.0.contains(data.kind.to_window_type().0) + } + + fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap> { + Some(&roots.kinds) + } +} diff --git a/src/it/test_config.rs b/src/it/test_config.rs index 621073ca..e942cead 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -126,6 +126,8 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) { ServerMessage::SwitchEvent { .. } => {} ServerMessage::ClientMatcherMatched { .. } => {} ServerMessage::ClientMatcherUnmatched { .. } => {} + ServerMessage::WindowMatcherMatched { .. } => {} + ServerMessage::WindowMatcherUnmatched { .. } => {} } } diff --git a/src/state.rs b/src/state.rs index 2af9769a..b0dd776f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,7 +15,7 @@ use { compositor::LIBEI_SOCKET, config::ConfigProxy, cpu_worker::CpuWorker, - criteria::clm::ClMatcherManager, + criteria::{clm::ClMatcherManager, tlm::TlMatcherManager}, cursor::{Cursor, ServerCursors}, cursor_user::{CursorUserGroup, CursorUserGroupId, CursorUserGroupIds, CursorUserIds}, damage::DamageVisualizer, @@ -243,6 +243,7 @@ pub struct State { pub icons: Icons, pub show_pin_icon: Cell, pub cl_matcher_manager: ClMatcherManager, + pub tl_matcher_manager: TlMatcherManager, } // impl Drop for State { @@ -952,6 +953,7 @@ impl State { self.toplevels.clear(); self.workspace_managers.clear(); self.cl_matcher_manager.clear(); + self.tl_matcher_manager.clear(); } pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index dc8c1857..316a3aba 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1,6 +1,10 @@ use { crate::{ client::{Client, ClientId}, + criteria::{ + CritDestroyListener, CritMatcherId, + tlm::{TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TlMatcherChange}, + }, ifs::{ ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, @@ -92,7 +96,10 @@ impl ToplevelNode for T { fn tl_set_parent(&self, parent: Rc) { let data = self.tl_data(); - data.parent.set(Some(parent.clone())); + let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); + if parent_was_none { + data.property_changed(TL_CHANGED_NEW); + } data.is_floating.set(parent.node_is_float()); self.tl_set_workspace(&parent.cnode_workspace()); } @@ -275,7 +282,6 @@ impl ToplevelType { } pub struct ToplevelData { - #[expect(dead_code)] pub node_id: NodeId, pub kind: ToplevelType, pub self_active: Cell, @@ -307,6 +313,8 @@ pub struct ToplevelData { pub ext_copy_sessions: CopyHashMap<(ClientId, ExtImageCopyCaptureSessionV1Id), Rc>, pub slf: Weak, + pub destroyed: CopyHashMap>>, + pub changed_properties: Cell, } impl ToplevelData { @@ -351,6 +359,8 @@ impl ToplevelData { jay_screencasts: Default::default(), ext_copy_sessions: Default::default(), slf: slf.clone(), + destroyed: Default::default(), + changed_properties: Default::default(), } } @@ -387,6 +397,20 @@ impl ToplevelData { (width, height) } + pub fn property_changed(&self, change: TlMatcherChange) { + let mgr = &self.state.tl_matcher_manager; + let props = self.changed_properties.get(); + if props.is_none() && mgr.has_no_interest(self, change) { + return; + } + self.changed_properties.set(props | change); + if props.is_none() && change.is_some() { + if let Some(node) = self.slf.upgrade() { + mgr.changed(node); + } + } + } + pub fn destroy_node(&self, node: &dyn Node) { for jay_tl in self.jay_toplevels.lock().drain_values() { jay_tl.destroy(); @@ -410,6 +434,7 @@ impl ToplevelData { } } self.detach_node(node); + self.property_changed(TL_CHANGED_DESTROYED); } pub fn detach_node(&self, node: &dyn Node) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index a19fb0a3..2360fc77 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -28,6 +28,7 @@ use { status::MessageFormat, theme::Color, video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, + window::WindowType, xwayland::XScalingMode, }, std::{ @@ -241,6 +242,20 @@ pub struct ClientMatch { pub exe_regex: Option, } +#[derive(Debug, Clone)] +pub struct WindowRule { + pub name: Option, + pub match_: WindowMatch, + pub action: Option, + pub latch: Option, +} + +#[derive(Default, Debug, Clone)] +pub struct WindowMatch { + pub generic: GenericMatch, + pub types: Option, +} + #[derive(Debug, Clone)] pub enum DrmDeviceMatch { Any(Vec), @@ -439,6 +454,7 @@ pub struct Config { pub named_actions: Vec, pub max_action_depth: u64, pub client_rules: Vec, + pub window_rules: Vec, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index ca1dc2e0..d49fabfc 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -39,6 +39,9 @@ mod tearing; mod theme; mod ui_drag; mod vrr; +mod window_match; +mod window_rule; +mod window_type; mod xwayland; #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 4baf4d36..f93723c7 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -32,6 +32,7 @@ use { theme::ThemeParser, ui_drag::UiDragParser, vrr::VrrParser, + window_rule::WindowRulesParser, xwayland::XwaylandParser, }, spanned::SpannedErrorExt, @@ -121,7 +122,14 @@ impl Parser for ConfigParser<'_> { ui_drag_val, xwayland_val, ), - (color_management_val, float_val, actions_val, max_action_depth_val, client_rules_val), + ( + color_management_val, + float_val, + actions_val, + max_action_depth_val, + client_rules_val, + window_rules_val, + ), ) = ext.extract(( ( opt(val("keymap")), @@ -165,6 +173,7 @@ impl Parser for ConfigParser<'_> { opt(val("actions")), recover(opt(int("max-action-depth"))), opt(val("clients")), + opt(val("windows")), ), ))?; let mut keymap = None; @@ -428,6 +437,13 @@ impl Parser for ConfigParser<'_> { Err(e) => log::warn!("Could not parse the client rules: {}", self.0.error(e)), } } + let mut window_rules = vec![]; + if let Some(value) = window_rules_val { + match value.parse(&mut WindowRulesParser(self.0)) { + Ok(v) => window_rules = v, + Err(e) => log::warn!("Could not parse the window rules: {}", self.0.error(e)), + } + } Ok(Config { keymap, repeat_rate, @@ -463,6 +479,7 @@ impl Parser for ConfigParser<'_> { named_actions, max_action_depth, client_rules, + window_rules, }) } } diff --git a/toml-config/src/config/parsers/window_match.rs b/toml-config/src/config/parsers/window_match.rs new file mode 100644 index 00000000..3c41403d --- /dev/null +++ b/toml-config/src/config/parsers/window_match.rs @@ -0,0 +1,113 @@ +use { + crate::{ + config::{ + GenericMatch, MatchExactly, WindowMatch, + context::Context, + extractor::{Extractor, ExtractorError, arr, n32, opt, str, val}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + parsers::window_type::{WindowTypeParser, WindowTypeParserError}, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum WindowMatchParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error(transparent)] + WindowTypes(#[from] WindowTypeParserError), +} + +pub struct WindowMatchParser<'a>(pub &'a Context<'a>); + +impl Parser for WindowMatchParser<'_> { + type Value = WindowMatch; + type Error = WindowMatchParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + 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 mut not = None; + if let Some(value) = not_val { + not = Some(Box::new(value.parse(&mut WindowMatchParser(self.0))?)); + } + macro_rules! list { + ($val:expr) => {{ + let mut list = None; + if let Some(value) = $val { + let mut res = vec![]; + for value in value.value { + res.push(value.parse(&mut WindowMatchParser(self.0))?); + } + list = Some(res); + } + list + }}; + } + let all = list!(all_val); + let any = list!(any_val); + let mut types = None; + if let Some(value) = types_val { + types = Some(value.parse_map(&mut WindowTypeParser)?); + } + let mut exactly = None; + if let Some(value) = exactly_val { + exactly = Some(value.parse(&mut WindowMatchExactlyParser(self.0))?); + } + Ok(WindowMatch { + generic: GenericMatch { + name: name.despan_into(), + not, + all, + any, + exactly, + }, + types, + }) + } +} + +pub struct WindowMatchExactlyParser<'a>(pub &'a Context<'a>); + +impl Parser for WindowMatchExactlyParser<'_> { + type Value = MatchExactly; + type Error = WindowMatchParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (num, list_val) = ext.extract((n32("num"), arr("list")))?; + let mut list = vec![]; + for el in list_val.value { + list.push(el.parse(&mut WindowMatchParser(self.0))?); + } + Ok(MatchExactly { + num: num.value as _, + list, + }) + } +} diff --git a/toml-config/src/config/parsers/window_rule.rs b/toml-config/src/config/parsers/window_rule.rs new file mode 100644 index 00000000..a31ab978 --- /dev/null +++ b/toml-config/src/config/parsers/window_rule.rs @@ -0,0 +1,104 @@ +use { + crate::{ + config::{ + WindowMatch, WindowRule, + context::Context, + extractor::{Extractor, ExtractorError, opt, str, val}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + parsers::{ + action::{ActionParser, ActionParserError}, + window_match::{WindowMatchParser, WindowMatchParserError}, + }, + spanned::SpannedErrorExt, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum WindowRuleParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Extract(#[from] ExtractorError), + #[error(transparent)] + Match(#[from] WindowMatchParserError), + #[error(transparent)] + Action(ActionParserError), + #[error(transparent)] + Latch(ActionParserError), +} + +pub struct WindowRuleParser<'a>(pub &'a Context<'a>); + +impl Parser for WindowRuleParser<'_> { + type Value = WindowRule; + type Error = WindowRuleParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (name, match_val, action_val, latch_val) = ext.extract(( + opt(str("name")), + opt(val("match")), + opt(val("action")), + opt(val("latch")), + ))?; + let mut action = None; + if let Some(value) = action_val { + action = Some( + value + .parse(&mut ActionParser(self.0)) + .map_spanned_err(WindowRuleParserError::Action)?, + ); + } + let mut latch = None; + if let Some(value) = latch_val { + latch = Some( + value + .parse(&mut ActionParser(self.0)) + .map_spanned_err(WindowRuleParserError::Latch)?, + ); + } + let match_ = match match_val { + None => WindowMatch::default(), + Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?, + }; + Ok(WindowRule { + name: name.despan_into(), + match_, + action, + latch, + }) + } +} + +pub struct WindowRulesParser<'a>(pub &'a Context<'a>); + +impl Parser for WindowRulesParser<'_> { + type Value = Vec; + type Error = WindowRuleParserError; + const EXPECTED: &'static [DataType] = &[DataType::Array]; + + fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { + let mut res = vec![]; + for el in array { + match el.parse(&mut WindowRuleParser(self.0)) { + Ok(o) => res.push(o), + Err(e) => { + log::warn!("Could not parse window rule: {}", self.0.error(e)); + } + } + } + Ok(res) + } +} diff --git a/toml-config/src/config/parsers/window_type.rs b/toml-config/src/config/parsers/window_type.rs new file mode 100644 index 00000000..388fe317 --- /dev/null +++ b/toml-config/src/config/parsers/window_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, window::WindowType}, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum WindowTypeParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown window type `{}`", .0)] + UnknownWindowType(String), +} + +pub struct WindowTypeParser; + +impl Parser for WindowTypeParser { + type Value = WindowType; + type Error = WindowTypeParserError; + const EXPECTED: &'static [DataType] = &[DataType::Array, DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let ty = match string { + "none" => WindowType(0), + "any" => WindowType(!0), + "container" => window::CONTAINER, + "placeholder" => window::PLACEHOLDER, + "xdg-toplevel" => window::XDG_TOPLEVEL, + "x-window" => window::X_WINDOW, + "client-window" => window::CLIENT_WINDOW, + _ => { + return Err( + WindowTypeParserError::UnknownWindowType(string.to_owned()).spanned(span) + ); + } + }; + Ok(ty) + } + + fn parse_array(&mut self, _span: Span, array: &[Spanned]) -> ParseResult { + let mut ty = WindowType(0); + for el in array { + ty |= el.parse(&mut WindowTypeParser)?; + } + Ok(ty) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index cd3afb22..b70cb692 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -14,7 +14,7 @@ use { config::{ Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut, - SimpleCommand, Status, Theme, parse_config, + SimpleCommand, Status, Theme, WindowRule, parse_config, }, rules::{MatcherTemp, RuleMapper}, }, @@ -47,6 +47,7 @@ use { on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, }, + window::Window, xwayland::set_x_scaling_mode, }, run_on_drop::on_drop, @@ -100,24 +101,39 @@ impl Action { }}; } let s = state.persistent.seat; + macro_rules! window_or_seat { + ($name:ident, $expr:expr) => {{ + let state = state.clone(); + B::new(move || { + if let Some($name) = state.window.get() { + if let Some($name) = $name { + $expr; + } + } else { + let $name = s; + $expr; + } + }) + }}; + } match self { Action::SimpleCommand { cmd } => match cmd { SimpleCommand::Focus(dir) => B::new(move || s.focus(dir)), - SimpleCommand::Move(dir) => B::new(move || s.move_(dir)), - SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)), - SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()), - SimpleCommand::SetSplit(b) => B::new(move || s.set_split(b)), - SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()), - SimpleCommand::SetMono(b) => B::new(move || s.set_mono(b)), - SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()), - SimpleCommand::SetFullscreen(b) => B::new(move || s.set_fullscreen(b)), + SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), + SimpleCommand::Split(axis) => window_or_seat!(s, s.create_split(axis)), + SimpleCommand::ToggleSplit => window_or_seat!(s, s.toggle_split()), + SimpleCommand::SetSplit(b) => window_or_seat!(s, s.set_split(b)), + SimpleCommand::ToggleMono => window_or_seat!(s, s.toggle_mono()), + SimpleCommand::SetMono(b) => window_or_seat!(s, s.set_mono(b)), + SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), + SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), SimpleCommand::FocusParent => B::new(move || s.focus_parent()), - SimpleCommand::Close => B::new(move || s.close()), + SimpleCommand::Close => window_or_seat!(s, s.close()), SimpleCommand::DisablePointerConstraint => { B::new(move || s.disable_pointer_constraint()) } - SimpleCommand::ToggleFloating => B::new(move || s.toggle_floating()), - SimpleCommand::SetFloating(b) => B::new(move || s.set_floating(b)), + SimpleCommand::ToggleFloating => window_or_seat!(s, s.toggle_floating()), + SimpleCommand::SetFloating(b) => window_or_seat!(s, s.set_floating(b)), SimpleCommand::Quit => B::new(quit), SimpleCommand::ReloadConfigToml => { let persistent = state.persistent.clone(); @@ -133,8 +149,10 @@ impl Action { B::new(move || set_float_above_fullscreen(bool)) } SimpleCommand::ToggleFloatAboveFullscreen => B::new(toggle_float_above_fullscreen), - SimpleCommand::SetFloatPinned(pinned) => B::new(move || s.set_float_pinned(pinned)), - SimpleCommand::ToggleFloatPinned => B::new(move || s.toggle_float_pinned()), + SimpleCommand::SetFloatPinned(pinned) => { + window_or_seat!(s, s.set_float_pinned(pinned)) + } + SimpleCommand::ToggleFloatPinned => window_or_seat!(s, s.toggle_float_pinned()), SimpleCommand::KillClient => client_action!(c, c.kill()), }, Action::Multi { actions } => { @@ -153,7 +171,7 @@ impl Action { } Action::MoveToWorkspace { name } => { let workspace = get_workspace(&name); - B::new(move || s.set_workspace(workspace)) + window_or_seat!(s, s.set_workspace(workspace)) } Action::ConfigureConnector { con } => B::new(move || { for c in connectors() { @@ -689,6 +707,8 @@ struct State { action_depth: Cell, client: Cell>, + + window: Cell>>, } impl Drop for State { @@ -897,13 +917,23 @@ impl State { fn with_client(&self, client: Client, check: bool, f: impl FnOnce()) { let mut opt = Some(client); - if check && client.does_not_exist() { + if client.0 == 0 || (check && client.does_not_exist()) { opt = None; } self.client.set(opt); f(); self.client.set(None); } + + fn with_window(&self, window: Window, check: bool, f: impl FnOnce()) { + let mut w = Some(window); + if check && !window.exists() { + w = None; + } + self.window.set(Some(w)); + f(); + self.window.set(None); + } } #[derive(Eq, PartialEq, Hash)] @@ -922,6 +952,7 @@ struct PersistentState { actions: RefCell, Rc>>, client_rules: Cell>>, client_rule_mapper: RefCell>>, + window_rules: Cell>>, } fn load_config(initial_load: bool, persistent: &Rc) { @@ -1003,10 +1034,13 @@ fn load_config(initial_load: bool, persistent: &Rc) { action_depth_max: config.max_action_depth, action_depth: Cell::new(0), client: Default::default(), + window: Default::default(), }); let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules); persistent.client_rules.set(client_rules); *state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper); + let (window_rules, _) = state.create_rules(&config.window_rules); + persistent.window_rules.set(window_rules); state.set_status(&config.status); persistent.actions.borrow_mut().clear(); for a in config.named_actions { @@ -1231,6 +1265,7 @@ pub fn configure() { actions: Default::default(), client_rules: Default::default(), client_rule_mapper: Default::default(), + window_rules: Default::default(), }); { let p = persistent.clone(); diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 468fdb41..7b5d256a 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -1,10 +1,13 @@ use { crate::{ State, - config::{ClientMatch, ClientRule, GenericMatch}, + config::{ClientMatch, ClientRule, GenericMatch, WindowMatch, WindowRule}, }, ahash::{AHashMap, AHashSet}, - jay_config::client::{ClientCriterion, ClientMatcher}, + jay_config::{ + client::{ClientCriterion, ClientMatcher}, + window::{WindowCriterion, WindowMatcher}, + }, std::{mem::ManuallyDrop, rc::Rc}, }; @@ -195,6 +198,131 @@ impl Rule for ClientRule { } } +impl Rule for WindowRule { + type Match = WindowMatch; + type Matcher = WindowMatcher; + type Criterion<'a> = WindowCriterion<'a>; + + const NAME_UPPER: &str = "Window"; + const NAME_LOWER: &str = "window"; + + fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + fn match_(&self) -> &Self::Match { + &self.match_ + } + + fn generic(m: &Self::Match) -> &GenericMatch { + &m.generic + } + + fn map_custom( + _state: &Rc, + all: &mut Vec>, + 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 { + all.push(m(WindowCriterion::$ty(value))); + } + }; + } + #[expect(unused_macros)] + macro_rules! bool { + ($ty:ident, $field:ident) => { + if let Some(value) = &match_.$field { + let crit = WindowCriterion::$ty; + let matcher = match value { + false => m(WindowCriterion::Not(&crit)), + true => m(crit), + }; + all.push(matcher); + } + }; + } + if let Some(value) = &match_.types { + all.push(m(WindowCriterion::Types(*value))); + } + Some(()) + } + + fn create(c: Self::Criterion<'_>) -> Self::Matcher { + c.to_matcher() + } + + fn destroy(m: Self::Matcher) { + m.destroy(); + } + + fn bind(&self, state: &Rc, matcher: Self::Matcher) { + let state = state.clone(); + macro_rules! latch { + ($g:ident, $client:ident, $win:ident) => { + let g = $g.clone(); + let state = state.clone(); + $win.latch(move || { + state.with_client($client, true, || { + state.with_window(*$win, true, || g()); + }); + }); + }; + } + if let Some(action) = &self.action { + let f = action.clone().into_fn(&state); + if let Some(action) = &self.latch { + let g = action.clone().into_rc_fn(&state); + matcher.bind(move |win| { + let client = win.client(); + state.with_client(client, false, || { + state.with_window(*win, false, &f); + }); + latch!(g, client, win); + }); + } else { + matcher.bind(move |win| { + let client = win.client(); + state.with_client(client, false, || { + state.with_window(*win, false, &f); + }); + }); + } + } else { + if let Some(action) = &self.latch { + let g = action.clone().into_rc_fn(&state); + matcher.bind(move |win| { + let client = win.client(); + latch!(g, client, win); + }); + } + } + } + + fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { + WindowCriterion::Matcher(m) + } + + fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a> { + WindowCriterion::Not(m) + } + + fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + WindowCriterion::All(m) + } + + fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + WindowCriterion::Any(m) + } + + fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> { + WindowCriterion::Exactly(n, m) + } +} + pub struct RuleMapper where R: Rule, diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index cbd4ed2c..1b562e07 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -858,6 +858,14 @@ "description": "", "$ref": "#/$defs/ClientRule" } + }, + "windows": { + "type": "array", + "description": "An array of window rules.\n\nThese rules can be used to give names to windows and to manipulate them.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n action = { type = \"move-to-workspace\", name = \"music\" }\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/WindowRule" + } } }, "required": [] @@ -1487,7 +1495,7 @@ }, "SimpleActionName": { "type": "string", - "description": "The name of a `simple` Action.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", + "description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `split-horizontal`\n- `split-vertical`\n- `toggle-split`\n- `tile-horizontal`\n- `tile-vertical`\n- `toggle-split`\n- `show-single`\n- `show-all`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n", "enum": [ "focus-left", "focus-down", @@ -1732,6 +1740,115 @@ "variant3" ] }, + "WindowMatch": { + "description": "Criteria for matching windows.\n\nIf no fields are set, all windows are matched. If multiple fields are set, all fields\nmust match the window.\n", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Matches if the window rule with this name matches.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n\n # Matches the same windows as the previous rule.\n [[windows]]\n match.name = \"spotify\"\n ```\n" + }, + "not": { + "description": "Matches if the contained criteria don't match.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"not-spotify\"\n match.not.title-regex = \"Spotify\"\n ```\n", + "$ref": "#/$defs/WindowMatch" + }, + "all": { + "type": "array", + "description": "Matches if all of the contained criteria match.\n\n- Example:\n\n ```toml\n [[windows]]\n match.all = [\n { title-regex = \"Spotify\" },\n { title-regex = \"Premium\" },\n ]\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/WindowMatch" + } + }, + "any": { + "type": "array", + "description": "Matches if any of the contained criteria match.\n\n- Example:\n\n ```toml\n [[windows]]\n match.any = [\n { title-regex = \"Spotify\" },\n { title-regex = \"Alacritty\" },\n ]\n ```\n", + "items": { + "description": "", + "$ref": "#/$defs/WindowMatch" + } + }, + "exactly": { + "description": "Matches if a specific number of contained criteria match.\n\n- Example:\n\n ```toml\n # Matches any window that is either Alacritty or on workspace 3 but not both.\n [[windows]]\n match.exactly.num = 1\n match.exactly.list = [\n { workspace = \"3\" },\n { title-regex = \"Alacritty\" },\n ]\n ```\n", + "$ref": "#/$defs/WindowMatchExactly" + }, + "types": { + "description": "Matches windows whose type is contained in the mask.", + "$ref": "#/$defs/WindowTypeMask" + } + }, + "required": [] + }, + "WindowMatchExactly": { + "description": "Criterion for matching a specific number of window criteria.\n", + "type": "object", + "properties": { + "num": { + "type": "number", + "description": "The number of criteria that must match." + }, + "list": { + "type": "array", + "description": "The list of criteria.", + "items": { + "description": "", + "$ref": "#/$defs/WindowMatch" + } + } + }, + "required": [ + "num", + "list" + ] + }, + "WindowRule": { + "description": "A window rule.\n", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of this rule.\n\nThis name can be referenced in other rules.\n\n- Example\n\n ```toml\n [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n\n [[windows]]\n match.name = \"spotify\"\n action = \"enter-fullscreen\"\n ```\n" + }, + "match": { + "description": "The criteria that select the window that this rule applies to.", + "$ref": "#/$defs/WindowMatch" + }, + "action": { + "description": "An action to execute when a window matches the criteria.", + "$ref": "#/$defs/Action" + }, + "latch": { + "description": "An action to execute when a window no longer matches the criteria.", + "$ref": "#/$defs/Action" + } + }, + "required": [] + }, + "WindowTypeMask": { + "description": "A mask of window types.\n", + "anyOf": [ + { + "type": "string", + "description": "A named mask.", + "enum": [ + "none", + "any", + "container", + "xdg-toplevel", + "x-window", + "client-window" + ] + }, + { + "type": "array", + "description": "An array of masks that are OR'd.", + "items": { + "description": "", + "$ref": "#/$defs/WindowTypeMask" + } + } + ] + }, "XScalingMode": { "type": "string", "description": "The scaling mode of X windows.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index cdf4d275..97245dfd 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1721,6 +1721,23 @@ The table has the following fields: The value of this field should be an array of [ClientRules](#types-ClientRule). +- `windows` (optional): + + An array of window rules. + + These rules can be used to give names to windows and to manipulate them. + + - Example: + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + action = { type = "move-to-workspace", name = "music" } + ``` + + The value of this field should be an array of [WindowRules](#types-WindowRule). + ### `Connector` @@ -3235,6 +3252,33 @@ The table has the following fields: The name of a `simple` Action. +When used inside a window rule, the following actions apply to the matched window +instead fo the focused window: + +- `move-left` +- `move-down` +- `move-up` +- `move-right` +- `split-horizontal` +- `split-vertical` +- `toggle-split` +- `tile-horizontal` +- `tile-vertical` +- `toggle-split` +- `show-single` +- `show-all` +- `toggle-fullscreen` +- `enter-fullscreen` +- `exit-fullscreen` +- `close` +- `toggle-floating` +- `float` +- `tile` +- `toggle-float-pinned` +- `pin-float` +- `unpin-float` + + - Example: ```toml @@ -3437,7 +3481,8 @@ The string should have one of the following values: Kills a client. - This action has no effect outside of client rules. + Within a window rule, it applies to the client of the window. Within a client rule + it applies to the matched client. Has no effect otherwise. @@ -3859,6 +3904,222 @@ The string should have one of the following values: + +### `WindowMatch` + +Criteria for matching windows. + +If no fields are set, all windows are matched. If multiple fields are set, all fields +must match the window. + +Values of this type should be tables. + +The table has the following fields: + +- `name` (optional): + + Matches if the window rule with this name matches. + + - Example: + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + + # Matches the same windows as the previous rule. + [[windows]] + match.name = "spotify" + ``` + + The value of this field should be a string. + +- `not` (optional): + + Matches if the contained criteria don't match. + + - Example: + + ```toml + [[windows]] + name = "not-spotify" + match.not.title-regex = "Spotify" + ``` + + The value of this field should be a [WindowMatch](#types-WindowMatch). + +- `all` (optional): + + Matches if all of the contained criteria match. + + - Example: + + ```toml + [[windows]] + match.all = [ + { title-regex = "Spotify" }, + { title-regex = "Premium" }, + ] + ``` + + The value of this field should be an array of [WindowMatchs](#types-WindowMatch). + +- `any` (optional): + + Matches if any of the contained criteria match. + + - Example: + + ```toml + [[windows]] + match.any = [ + { title-regex = "Spotify" }, + { title-regex = "Alacritty" }, + ] + ``` + + The value of this field should be an array of [WindowMatchs](#types-WindowMatch). + +- `exactly` (optional): + + Matches if a specific number of contained criteria match. + + - Example: + + ```toml + # Matches any window that is either Alacritty or on workspace 3 but not both. + [[windows]] + match.exactly.num = 1 + match.exactly.list = [ + { workspace = "3" }, + { title-regex = "Alacritty" }, + ] + ``` + + The value of this field should be a [WindowMatchExactly](#types-WindowMatchExactly). + +- `types` (optional): + + Matches windows whose type is contained in the mask. + + The value of this field should be a [WindowTypeMask](#types-WindowTypeMask). + + + +### `WindowMatchExactly` + +Criterion for matching a specific number of window criteria. + +Values of this type should be tables. + +The table has the following fields: + +- `num` (required): + + The number of criteria that must match. + + The value of this field should be a number. + +- `list` (required): + + The list of criteria. + + The value of this field should be an array of [WindowMatchs](#types-WindowMatch). + + + +### `WindowRule` + +A window rule. + +Values of this type should be tables. + +The table has the following fields: + +- `name` (optional): + + The name of this rule. + + This name can be referenced in other rules. + + - Example + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + + [[windows]] + match.name = "spotify" + action = "enter-fullscreen" + ``` + + The value of this field should be a string. + +- `match` (optional): + + The criteria that select the window that this rule applies to. + + The value of this field should be a [WindowMatch](#types-WindowMatch). + +- `action` (optional): + + An action to execute when a window matches the criteria. + + The value of this field should be a [Action](#types-Action). + +- `latch` (optional): + + An action to execute when a window no longer matches the criteria. + + The value of this field should be a [Action](#types-Action). + + + +### `WindowTypeMask` + +A mask of window 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 empty mask. + +- `any`: + + The mask containing every possible type. + +- `container`: + + The mask matching a container. + +- `xdg-toplevel`: + + The mask matching an XDG toplevel. + +- `x-window`: + + The mask matching an X window. + +- `client-window`: + + The mask matching any type of client window. + + +#### An array + +An array of masks that are OR'd. + +Each element of this array should be a [WindowTypeMask](#types-WindowTypeMask). + + ### `XScalingMode` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 2b3659e5..c243a6aa 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -691,6 +691,33 @@ Exec: SimpleActionName: description: | The name of a `simple` Action. + + When used inside a window rule, the following actions apply to the matched window + instead fo the focused window: + + - `move-left` + - `move-down` + - `move-up` + - `move-right` + - `split-horizontal` + - `split-vertical` + - `toggle-split` + - `tile-horizontal` + - `tile-vertical` + - `toggle-split` + - `show-single` + - `show-all` + - `toggle-fullscreen` + - `enter-fullscreen` + - `exit-fullscreen` + - `close` + - `toggle-floating` + - `float` + - `tile` + - `toggle-float-pinned` + - `pin-float` + - `unpin-float` + - Example: @@ -825,7 +852,8 @@ SimpleActionName: description: | Kills a client. - This action has no effect outside of client rules. + Within a window rule, it applies to the client of the window. Within a client rule + it applies to the matched client. Has no effect otherwise. Color: @@ -2509,6 +2537,24 @@ Config: name = "spotify" match.sandbox-app-id = "com.spotify.Client" ``` + windows: + kind: array + items: + ref: WindowRule + required: false + description: | + An array of window rules. + + These rules can be used to give names to windows and to manipulate them. + + - Example: + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + action = { type = "move-to-workspace", name = "music" } + ``` Idle: @@ -3284,3 +3330,179 @@ ClientMatchExactly: ref: ClientMatch required: true description: The list of criteria. + + +WindowRule: + kind: table + description: | + A window rule. + fields: + name: + kind: string + required: false + description: | + The name of this rule. + + This name can be referenced in other rules. + + - Example + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + + [[windows]] + match.name = "spotify" + action = "enter-fullscreen" + ``` + match: + ref: WindowMatch + required: false + description: The criteria that select the window that this rule applies to. + action: + ref: Action + required: false + description: An action to execute when a window matches the criteria. + latch: + ref: Action + required: false + description: An action to execute when a window no longer matches the criteria. + + +WindowMatch: + kind: table + description: | + Criteria for matching windows. + + If no fields are set, all windows are matched. If multiple fields are set, all fields + must match the window. + fields: + name: + kind: string + required: false + description: | + Matches if the window rule with this name matches. + + - Example: + + ```toml + [[windows]] + name = "spotify" + match.title-regex = "Spotify" + + # Matches the same windows as the previous rule. + [[windows]] + match.name = "spotify" + ``` + not: + ref: WindowMatch + required: false + description: | + Matches if the contained criteria don't match. + + - Example: + + ```toml + [[windows]] + name = "not-spotify" + match.not.title-regex = "Spotify" + ``` + all: + kind: array + items: + ref: WindowMatch + required: false + description: | + Matches if all of the contained criteria match. + + - Example: + + ```toml + [[windows]] + match.all = [ + { title-regex = "Spotify" }, + { title-regex = "Premium" }, + ] + ``` + any: + kind: array + items: + ref: WindowMatch + required: false + description: | + Matches if any of the contained criteria match. + + - Example: + + ```toml + [[windows]] + match.any = [ + { title-regex = "Spotify" }, + { title-regex = "Alacritty" }, + ] + ``` + exactly: + ref: WindowMatchExactly + required: false + description: | + Matches if a specific number of contained criteria match. + + - Example: + + ```toml + # Matches any window that is either Alacritty or on workspace 3 but not both. + [[windows]] + match.exactly.num = 1 + match.exactly.list = [ + { workspace = "3" }, + { title-regex = "Alacritty" }, + ] + ``` + types: + ref: WindowTypeMask + required: false + description: Matches windows whose type is contained in the mask. + + +WindowMatchExactly: + kind: table + description: | + Criterion for matching a specific number of window criteria. + fields: + num: + kind: number + required: true + description: The number of criteria that must match. + list: + kind: array + items: + ref: WindowMatch + required: true + description: The list of criteria. + + +WindowTypeMask: + description: | + A mask of window types. + kind: variable + variants: + - kind: string + description: A named mask. + values: + - value: none + description: The empty mask. + - value: any + description: The mask containing every possible type. + - value: container + description: The mask matching a container. + - value: xdg-toplevel + description: The mask matching an XDG toplevel. + - value: x-window + description: The mask matching an X window. + - value: client-window + description: The mask matching any type of client window. + - kind: array + description: An array of masks that are OR'd. + items: + ref: WindowTypeMask From 2b5be7fbd931ff89b6414654d0155a9ec0623c5b Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 2 May 2025 23:46:11 +0200 Subject: [PATCH 18/35] config: add client window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 8 ++ jay-config/src/window.rs | 7 +- src/compositor.rs | 1 + src/config/handler.rs | 4 + src/criteria/crit_graph.rs | 4 +- src/criteria/crit_graph/crit_upstream.rs | 1 - src/criteria/tlm.rs | 10 +- src/criteria/tlm/tlm_matchers.rs | 1 + src/criteria/tlm/tlm_matchers/tlmm_client.rs | 117 ++++++++++++++++++ src/ifs/wl_surface/x_surface/xwindow.rs | 7 ++ src/state.rs | 2 + src/xwayland/xwm.rs | 1 + toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 30 +++-- toml-config/src/rules.rs | 10 +- toml-spec/spec/spec.generated.json | 4 + toml-spec/spec/spec.generated.md | 6 + toml-spec/spec/spec.yaml | 4 + 19 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_client.rs 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: From 6ef7655dbd809099368299c89179722f23c0e082 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:22:33 +0200 Subject: [PATCH 19/35] config: add title window criteria --- jay-config/src/_private.rs | 4 ++- jay-config/src/_private/client.rs | 6 ++-- jay-config/src/window.rs | 4 +++ src/config/handler.rs | 7 ++-- src/criteria/tlm.rs | 18 +++++++--- src/criteria/tlm/tlm_matchers.rs | 1 + src/criteria/tlm/tlm_matchers/tlmm_string.rs | 23 +++++++++++++ src/tree/toplevel.rs | 3 +- toml-config/src/config.rs | 2 ++ .../src/config/parsers/window_match.rs | 33 +++++++++++++------ toml-config/src/rules.rs | 3 +- toml-spec/spec/spec.generated.json | 8 +++++ toml-spec/spec/spec.generated.md | 12 +++++++ toml-spec/spec/spec.yaml | 8 +++++ 14 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_string.rs 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: From da64166e82fe6889cf3ce3caf9abcfc24dfbd07f Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:27:42 +0200 Subject: [PATCH 20/35] config: add app-id window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 2 + jay-config/src/window.rs | 4 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 12 ++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 12 +++++ src/tree/toplevel.rs | 12 ++++- toml-config/src/config.rs | 2 + .../src/config/parsers/window_match.rs | 50 +++++++++++-------- 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, 102 insertions(+), 24 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index bd9c227a..c25e69f0 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -115,4 +115,5 @@ pub enum WindowCriterionIpc { #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] pub enum WindowCriterionStringField { Title, + AppId, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2bfc0633..7a927f95 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1655,6 +1655,8 @@ impl ConfigClient { } WindowCriterion::Title(t) => string!(t, Title, false), WindowCriterion::TitleRegex(t) => string!(t, Title, true), + WindowCriterion::AppId(t) => string!(t, AppId, false), + WindowCriterion::AppIdRegex(t) => string!(t, AppId, 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 18c8b35c..7367793e 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -240,6 +240,10 @@ pub enum WindowCriterion<'a> { Title(&'a str), /// Matches the title of the window with a regular expression. TitleRegex(&'a str), + /// Matches the app-id of the window verbatim. + AppId(&'a str), + /// Matches the app-id of the window with a regular expression. + AppIdRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index dae15dc4..47bceb4b 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1989,6 +1989,7 @@ impl ConfigProxyHandler { }; match *field { WindowCriterionStringField::Title => mgr.title(needle), + WindowCriterionStringField::AppId => mgr.app_id(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index c751d64c..42429f7b 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -10,7 +10,9 @@ use { crit_leaf::{CritLeafEvent, CritLeafMatcher}, crit_matchers::critm_constant::CritMatchConstant, tlm::tlm_matchers::{ - tlmm_client::TlmMatchClient, tlmm_kind::TlmMatchKind, tlmm_string::TlmMatchTitle, + tlmm_client::TlmMatchClient, + tlmm_kind::TlmMatchKind, + tlmm_string::{TlmMatchAppId, TlmMatchTitle}, }, }, state::State, @@ -29,6 +31,7 @@ bitflags! { TL_CHANGED_DESTROYED = 1 << 0, TL_CHANGED_NEW = 1 << 1, TL_CHANGED_TITLE = 1 << 2, + TL_CHANGED_APP_ID = 1 << 3, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -48,6 +51,7 @@ pub struct RootMatchers { kinds: TlmRootMatcherMap, clients: CopyHashMap>, title: TlmRootMatcherMap, + app_id: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -143,6 +147,7 @@ impl TlMatcherManager { }; } conditional!(TL_CHANGED_TITLE, title); + conditional!(TL_CHANGED_APP_ID, app_id); false } @@ -210,12 +215,17 @@ impl TlMatcherManager { }; } conditional!(TL_CHANGED_TITLE, title); + conditional!(TL_CHANGED_APP_ID, app_id); } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { self.root(TlmMatchTitle::new(string)) } + pub fn app_id(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchAppId::new(string)) + } + pub fn kind(&self, kind: WindowType) -> Rc { self.root(TlmMatchKind::new(kind)) } diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index 3a56edce..967d8032 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -9,8 +9,10 @@ use crate::{ pub type TlmMatchString = CritMatchString; pub type TlmMatchTitle = TlmMatchString; +pub type TlmMatchAppId = TlmMatchString; pub struct TitleAccess; +pub struct AppIdAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -21,3 +23,13 @@ impl StringAccess for TitleAccess { &roots.title } } + +impl StringAccess for AppIdAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + f(&data.app_id.borrow()) + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.app_id + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 2f53adc7..109d6edb 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -3,7 +3,10 @@ use { client::{Client, ClientId}, criteria::{ CritDestroyListener, CritMatcherId, - tlm::{TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TL_CHANGED_TITLE, TlMatcherChange}, + tlm::{ + TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TL_CHANGED_TITLE, + TlMatcherChange, + }, }, ifs::{ ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, @@ -498,11 +501,16 @@ impl ToplevelData { } pub fn set_app_id(&self, app_id: &str) { - *self.app_id.borrow_mut() = app_id.to_string(); + let dst = &mut *self.app_id.borrow_mut(); + if *dst == app_id { + return; + } + *dst = app_id.to_string(); for handle in self.handles.lock().values() { handle.send_app_id(app_id); handle.send_done(); } + self.property_changed(TL_CHANGED_APP_ID) } pub fn set_fullscreen( diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index e7eb3342..a6600007 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -257,6 +257,8 @@ pub struct WindowMatch { pub client: Option, pub title: Option, pub title_regex: Option, + pub app_id: Option, + pub app_id_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 f4594bd6..2a89c19a 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -44,27 +44,33 @@ 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, - 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 ( + ( + name, + not_val, + all_val, + any_val, + exactly_val, + types_val, + client_val, + title, + title_regex, + ), + (app_id, app_id_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")), + ), + (opt(str("app-id")), opt(str("app-id-regex"))), + ))?; let mut not = None; if let Some(value) = not_val { not = Some(Box::new(value.parse(&mut WindowMatchParser(self.0))?)); @@ -106,6 +112,8 @@ impl Parser for WindowMatchParser<'_> { }, title: title.despan_into(), title_regex: title_regex.despan_into(), + app_id: app_id.despan_into(), + app_id_regex: app_id_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index e3abb988..860474fa 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -257,6 +257,8 @@ impl Rule for WindowRule { } value!(Title, title); value!(TitleRegex, title_regex); + value!(AppId, app_id); + value!(AppIdRegex, app_id_regex); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 78a37b49..ad17f5b6 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1787,6 +1787,14 @@ "title-regex": { "type": "string", "description": "Matches the title of the window with a regular expression." + }, + "app-id": { + "type": "string", + "description": "Matches the app-id of the window verbatim." + }, + "app-id-regex": { + "type": "string", + "description": "Matches the app-id 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 d9a1023f..8dcf8e8b 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4022,6 +4022,18 @@ The table has the following fields: The value of this field should be a string. +- `app-id` (optional): + + Matches the app-id of the window verbatim. + + The value of this field should be a string. + +- `app-id-regex` (optional): + + Matches the app-id 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 1282eb43..a77ee364 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3475,6 +3475,14 @@ WindowMatch: kind: string required: false description: Matches the title of the window with a regular expression. + app-id: + kind: string + required: false + description: Matches the app-id of the window verbatim. + app-id-regex: + kind: string + required: false + description: Matches the app-id of the window with a regular expression. WindowMatchExactly: From 8bb8b2a649e10dc1669fa158c8142758dd0238b6 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:31:59 +0200 Subject: [PATCH 21/35] config: add floating window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 35 +++++++++++++++---- src/criteria/tlm/tlm_matchers.rs | 2 +- .../tlm/tlm_matchers/tlmm_floating.rs | 11 ++++++ src/tree/toplevel.rs | 11 ++++-- toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 11 ++++-- toml-config/src/rules.rs | 2 +- toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 6 ++++ toml-spec/spec/spec.yaml | 4 +++ 14 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_floating.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index c25e69f0..534531db 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -110,6 +110,7 @@ pub enum WindowCriterionIpc { }, Types(WindowType), Client(ClientMatcher), + Floating, } #[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 7a927f95..a0288cac 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1657,6 +1657,7 @@ impl ConfigClient { WindowCriterion::TitleRegex(t) => string!(t, Title, true), WindowCriterion::AppId(t) => string!(t, AppId, false), WindowCriterion::AppIdRegex(t) => string!(t, AppId, true), + WindowCriterion::Floating => WindowCriterionIpc::Floating, }; 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 7367793e..cbada630 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -244,6 +244,8 @@ pub enum WindowCriterion<'a> { AppId(&'a str), /// Matches the app-id of the window with a regular expression. AppIdRegex(&'a str), + /// Matches if the window is floating. + Floating, } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 47bceb4b..6b03c7f2 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1997,6 +1997,7 @@ impl ConfigProxyHandler { self.state.cl_matcher_manager.rematch_all(&self.state); mgr.client(&self.state, &self.get_client_matcher(*c)?.node) } + WindowCriterionIpc::Floating => mgr.floating(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 42429f7b..f8c5a88b 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -6,11 +6,14 @@ use { CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode, FixedRootMatcher, RootMatcherMap, clm::ClmUpstreamNode, - crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner}, + crit_graph::{ + CritMgr, CritRoot, CritRootFixed, CritTarget, CritTargetOwner, WeakCritTargetOwner, + }, crit_leaf::{CritLeafEvent, CritLeafMatcher}, crit_matchers::critm_constant::CritMatchConstant, tlm::tlm_matchers::{ tlmm_client::TlmMatchClient, + tlmm_floating::TlmMatchFloating, tlmm_kind::TlmMatchKind, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, }, @@ -23,7 +26,11 @@ use { }, }, jay_config::window::WindowType, - std::rc::{Rc, Weak}, + linearize::static_map, + std::{ + marker::PhantomData, + rc::{Rc, Weak}, + }, }; bitflags! { @@ -32,6 +39,7 @@ bitflags! { TL_CHANGED_NEW = 1 << 1, TL_CHANGED_TITLE = 1 << 2, TL_CHANGED_APP_ID = 1 << 3, + TL_CHANGED_FLOATING = 1 << 4, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -41,6 +49,7 @@ pub struct TlMatcherManager { changes: AsyncQueue>, leaf_events: Rc>>, constant: TlmFixedRootMatcher>, + floating: TlmFixedRootMatcher, matchers: Rc, } @@ -78,8 +87,20 @@ pub type TlmLeafMatcher = CritLeafMatcher; impl TlMatcherManager { pub fn new(ids: &Rc) -> Self { let matchers = Rc::new(RootMatchers::default()); + macro_rules! bool { + ($name:ident) => {{ + static_map! { + v => CritRoot::new( + &matchers, + ids.next(), + CritRootFixed($name(v), PhantomData), + ) + } + }}; + } Self { constant: CritMatchConstant::create(&matchers, ids), + floating: bool!(TlmMatchFloating), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -108,7 +129,6 @@ impl TlMatcherManager { if change.contains(TL_CHANGED_DESTROYED) && data.destroyed.is_not_empty() { return true; } - #[expect(unused_macros)] macro_rules! fixed { ($name:ident) => { if self.$name[false].has_downstream() || self.$name[true].has_downstream() { @@ -138,7 +158,6 @@ impl TlMatcherManager { } }; } - #[expect(unused_macros)] macro_rules! fixed_conditional { ($change:expr, $field:ident) => { if change.contains($change) { @@ -148,6 +167,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + fixed_conditional!(TL_CHANGED_FLOATING, floating); false } @@ -177,7 +197,6 @@ impl TlMatcherManager { .filter_map(|m| m.upgrade()) }; } - #[expect(unused_macros)] macro_rules! fixed { ($name:ident) => { self.$name[false].handle(data); @@ -206,7 +225,6 @@ impl TlMatcherManager { } }; } - #[expect(unused_macros)] macro_rules! fixed_conditional { ($change:expr, $field:ident) => { if changed.contains($change) { @@ -216,6 +234,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + fixed_conditional!(TL_CHANGED_FLOATING, floating); } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -226,6 +245,10 @@ impl TlMatcherManager { self.root(TlmMatchAppId::new(string)) } + pub fn floating(&self) -> Rc { + self.floating[true].clone() + } + pub fn kind(&self, kind: WindowType) -> Rc { self.root(TlmMatchKind::new(kind)) } diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index d40d6dce..44a5ab0f 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -1,4 +1,3 @@ -#[expect(unused_macros)] macro_rules! fixed_root_criterion { ($ty:ty, $field:ident) => { impl crate::criteria::crit_graph::CritFixedRootCriterionBase @@ -19,5 +18,6 @@ macro_rules! fixed_root_criterion { } pub mod tlmm_client; +pub mod tlmm_floating; pub mod tlmm_kind; pub mod tlmm_string; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_floating.rs b/src/criteria/tlm/tlm_matchers/tlmm_floating.rs new file mode 100644 index 00000000..0386fa0a --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_floating.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchFloating(pub bool); + +fixed_root_criterion!(TlmMatchFloating, floating); + +impl CritFixedRootCriterion for TlmMatchFloating { + fn matches(&self, data: &ToplevelData) -> bool { + data.is_floating.get() + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 109d6edb..6abfbd68 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -4,8 +4,8 @@ use { criteria::{ CritDestroyListener, CritMatcherId, tlm::{ - TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TL_CHANGED_TITLE, - TlMatcherChange, + TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, TL_CHANGED_NEW, + TL_CHANGED_TITLE, TlMatcherChange, }, }, ifs::{ @@ -104,7 +104,12 @@ impl ToplevelNode for T { if parent_was_none { data.property_changed(TL_CHANGED_NEW); } - data.is_floating.set(parent.node_is_float()); + let was_floating = data.is_floating.get(); + let is_floating = parent.node_is_float(); + if was_floating != is_floating { + data.property_changed(TL_CHANGED_FLOATING); + } + data.is_floating.set(is_floating); self.tl_set_workspace(&parent.cnode_workspace()); } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index a6600007..f53f89a1 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -259,6 +259,7 @@ pub struct WindowMatch { pub title_regex: Option, pub app_id: Option, pub app_id_regex: Option, + pub floating: 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 2a89c19a..c3107a46 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -3,7 +3,7 @@ use { config::{ GenericMatch, MatchExactly, WindowMatch, context::Context, - extractor::{Extractor, ExtractorError, arr, n32, opt, str, val}, + extractor::{Extractor, ExtractorError, arr, bol, n32, opt, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ client_match::{ClientMatchParser, ClientMatchParserError}, @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex), + (app_id, app_id_regex, floating), ) = ext.extract(( ( opt(str("name")), @@ -69,7 +69,11 @@ impl Parser for WindowMatchParser<'_> { opt(str("title")), opt(str("title-regex")), ), - (opt(str("app-id")), opt(str("app-id-regex"))), + ( + opt(str("app-id")), + opt(str("app-id-regex")), + opt(bol("floating")), + ), ))?; let mut not = None; if let Some(value) = not_val { @@ -114,6 +118,7 @@ impl Parser for WindowMatchParser<'_> { title_regex: title_regex.despan_into(), app_id: app_id.despan_into(), app_id_regex: app_id_regex.despan_into(), + floating: floating.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 860474fa..93e59479 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -231,7 +231,6 @@ impl Rule for WindowRule { } }; } - #[expect(unused_macros)] macro_rules! bool { ($ty:ident, $field:ident) => { if let Some(value) = &match_.$field { @@ -259,6 +258,7 @@ impl Rule for WindowRule { value!(TitleRegex, title_regex); value!(AppId, app_id); value!(AppIdRegex, app_id_regex); + bool!(Floating, floating); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index ad17f5b6..3b2f52c3 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1795,6 +1795,10 @@ "app-id-regex": { "type": "string", "description": "Matches the app-id of the window with a regular expression." + }, + "floating": { + "type": "boolean", + "description": "Matches if the window is/isn't floating." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 8dcf8e8b..98db444c 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4034,6 +4034,12 @@ The table has the following fields: The value of this field should be a string. +- `floating` (optional): + + Matches if the window is/isn't floating. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index a77ee364..76ea9382 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3483,6 +3483,10 @@ WindowMatch: kind: string required: false description: Matches the app-id of the window with a regular expression. + floating: + kind: boolean + required: false + description: Matches if the window is/isn't floating. WindowMatchExactly: From dcf57db3df51970e6244966bacbd06cbf887b113 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:35:29 +0200 Subject: [PATCH 22/35] config: add visibility window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 10 ++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + src/criteria/tlm/tlm_matchers/tlmm_visible.rs | 11 +++++++++++ src/tree/toplevel.rs | 6 ++++-- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/window_match.rs | 4 +++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 6 ++++++ toml-spec/spec/spec.yaml | 4 ++++ 14 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_visible.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 534531db..6d0c1293 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -111,6 +111,7 @@ pub enum WindowCriterionIpc { Types(WindowType), Client(ClientMatcher), Floating, + Visible, } #[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 a0288cac..5ab093bd 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1658,6 +1658,7 @@ impl ConfigClient { WindowCriterion::AppId(t) => string!(t, AppId, false), WindowCriterion::AppIdRegex(t) => string!(t, AppId, true), WindowCriterion::Floating => WindowCriterionIpc::Floating, + WindowCriterion::Visible => WindowCriterionIpc::Visible, }; 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 cbada630..9415fed9 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -246,6 +246,8 @@ pub enum WindowCriterion<'a> { AppIdRegex(&'a str), /// Matches if the window is floating. Floating, + /// Matches if the window is visible. + Visible, } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 6b03c7f2..a98b36fa 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1998,6 +1998,7 @@ impl ConfigProxyHandler { mgr.client(&self.state, &self.get_client_matcher(*c)?.node) } WindowCriterionIpc::Floating => mgr.floating(), + WindowCriterionIpc::Visible => mgr.visible(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index f8c5a88b..a24dfff9 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -16,6 +16,7 @@ use { tlmm_floating::TlmMatchFloating, tlmm_kind::TlmMatchKind, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, + tlmm_visible::TlmMatchVisible, }, }, state::State, @@ -40,6 +41,7 @@ bitflags! { TL_CHANGED_TITLE = 1 << 2, TL_CHANGED_APP_ID = 1 << 3, TL_CHANGED_FLOATING = 1 << 4, + TL_CHANGED_VISIBLE = 1 << 5, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -50,6 +52,7 @@ pub struct TlMatcherManager { leaf_events: Rc>>, constant: TlmFixedRootMatcher>, floating: TlmFixedRootMatcher, + visible: TlmFixedRootMatcher, matchers: Rc, } @@ -101,6 +104,7 @@ impl TlMatcherManager { Self { constant: CritMatchConstant::create(&matchers, ids), floating: bool!(TlmMatchFloating), + visible: bool!(TlmMatchVisible), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -168,6 +172,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); fixed_conditional!(TL_CHANGED_FLOATING, floating); + fixed_conditional!(TL_CHANGED_VISIBLE, visible); false } @@ -235,6 +240,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); fixed_conditional!(TL_CHANGED_FLOATING, floating); + fixed_conditional!(TL_CHANGED_VISIBLE, visible); } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -256,6 +262,10 @@ impl TlMatcherManager { pub fn client(&self, state: &Rc, client: &Rc) -> Rc { TlmMatchClient::new(state, client) } + + pub fn visible(&self) -> Rc { + self.visible[true].clone() + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 44a5ab0f..bb84be5b 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -21,3 +21,4 @@ pub mod tlmm_client; pub mod tlmm_floating; pub mod tlmm_kind; pub mod tlmm_string; +pub mod tlmm_visible; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_visible.rs b/src/criteria/tlm/tlm_matchers/tlmm_visible.rs new file mode 100644 index 00000000..1bdea414 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_visible.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchVisible(pub bool); + +fixed_root_criterion!(TlmMatchVisible, visible); + +impl CritFixedRootCriterion for TlmMatchVisible { + fn matches(&self, data: &ToplevelData) -> bool { + data.visible.get() + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 6abfbd68..fc5886dc 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -5,7 +5,7 @@ use { CritDestroyListener, CritMatcherId, tlm::{ TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, TL_CHANGED_NEW, - TL_CHANGED_TITLE, TlMatcherChange, + TL_CHANGED_TITLE, TL_CHANGED_VISIBLE, TlMatcherChange, }, }, ifs::{ @@ -631,7 +631,9 @@ impl ToplevelData { } pub fn set_visible(&self, node: &dyn Node, visible: bool) { - self.visible.set(visible); + if self.visible.replace(visible) != visible { + self.property_changed(TL_CHANGED_VISIBLE); + } self.seat_state.set_visible(node, visible); for sc in self.jay_screencasts.lock().values() { sc.update_latch_listener(); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index f53f89a1..f84a4034 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -260,6 +260,7 @@ pub struct WindowMatch { pub app_id: Option, pub app_id_regex: Option, pub floating: Option, + pub visible: 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 c3107a46..1a2b1f3e 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating), + (app_id, app_id_regex, floating, visible), ) = ext.extract(( ( opt(str("name")), @@ -73,6 +73,7 @@ impl Parser for WindowMatchParser<'_> { opt(str("app-id")), opt(str("app-id-regex")), opt(bol("floating")), + opt(bol("visible")), ), ))?; let mut not = None; @@ -119,6 +120,7 @@ impl Parser for WindowMatchParser<'_> { app_id: app_id.despan_into(), app_id_regex: app_id_regex.despan_into(), floating: floating.despan(), + visible: visible.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 93e59479..2a7de27a 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -259,6 +259,7 @@ impl Rule for WindowRule { value!(AppId, app_id); value!(AppIdRegex, app_id_regex); bool!(Floating, floating); + bool!(Visible, visible); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 3b2f52c3..6b21ec88 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1799,6 +1799,10 @@ "floating": { "type": "boolean", "description": "Matches if the window is/isn't floating." + }, + "visible": { + "type": "boolean", + "description": "Matches if the window is/isn't visible." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 98db444c..cb55fd88 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4040,6 +4040,12 @@ The table has the following fields: The value of this field should be a boolean. +- `visible` (optional): + + Matches if the window is/isn't visible. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 76ea9382..4bcdbd34 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3487,6 +3487,10 @@ WindowMatch: kind: boolean required: false description: Matches if the window is/isn't floating. + visible: + kind: boolean + required: false + description: Matches if the window is/isn't visible. WindowMatchExactly: From eb172e9d8cc5b650467d1541627d0920536bfe0b Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:38:57 +0200 Subject: [PATCH 23/35] config: add urgency window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 10 ++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + src/criteria/tlm/tlm_matchers/tlmm_urgent.rs | 11 +++++++++++ src/tree/container.rs | 2 +- src/tree/toplevel.rs | 12 +++++++++--- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/window_match.rs | 4 +++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 6 ++++++ toml-spec/spec/spec.yaml | 4 ++++ 15 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_urgent.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 6d0c1293..bd7bc94a 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -112,6 +112,7 @@ pub enum WindowCriterionIpc { Client(ClientMatcher), Floating, Visible, + Urgent, } #[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 5ab093bd..0881d6bb 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1659,6 +1659,7 @@ impl ConfigClient { WindowCriterion::AppIdRegex(t) => string!(t, AppId, true), WindowCriterion::Floating => WindowCriterionIpc::Floating, WindowCriterion::Visible => WindowCriterionIpc::Visible, + WindowCriterion::Urgent => WindowCriterionIpc::Urgent, }; 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 9415fed9..0d3e1127 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -248,6 +248,8 @@ pub enum WindowCriterion<'a> { Floating, /// Matches if the window is visible. Visible, + /// Matches if the window has the urgency flag set. + Urgent, } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index a98b36fa..bdd5fb66 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1999,6 +1999,7 @@ impl ConfigProxyHandler { } WindowCriterionIpc::Floating => mgr.floating(), WindowCriterionIpc::Visible => mgr.visible(), + WindowCriterionIpc::Urgent => mgr.urgent(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index a24dfff9..af9797db 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -16,6 +16,7 @@ use { tlmm_floating::TlmMatchFloating, tlmm_kind::TlmMatchKind, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, + tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, }, @@ -42,6 +43,7 @@ bitflags! { TL_CHANGED_APP_ID = 1 << 3, TL_CHANGED_FLOATING = 1 << 4, TL_CHANGED_VISIBLE = 1 << 5, + TL_CHANGED_URGENT = 1 << 6, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -53,6 +55,7 @@ pub struct TlMatcherManager { constant: TlmFixedRootMatcher>, floating: TlmFixedRootMatcher, visible: TlmFixedRootMatcher, + urgent: TlmFixedRootMatcher, matchers: Rc, } @@ -105,6 +108,7 @@ impl TlMatcherManager { constant: CritMatchConstant::create(&matchers, ids), floating: bool!(TlmMatchFloating), visible: bool!(TlmMatchVisible), + urgent: bool!(TlmMatchUrgent), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -173,6 +177,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); + fixed_conditional!(TL_CHANGED_URGENT, urgent); false } @@ -241,6 +246,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); + fixed_conditional!(TL_CHANGED_URGENT, urgent); } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -266,6 +272,10 @@ impl TlMatcherManager { pub fn visible(&self) -> Rc { self.visible[true].clone() } + + pub fn urgent(&self) -> Rc { + self.urgent[true].clone() + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index bb84be5b..fcff929e 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -21,4 +21,5 @@ pub mod tlmm_client; pub mod tlmm_floating; pub mod tlmm_kind; pub mod tlmm_string; +pub mod tlmm_urgent; pub mod tlmm_visible; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_urgent.rs b/src/criteria/tlm/tlm_matchers/tlmm_urgent.rs new file mode 100644 index 00000000..157135ad --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_urgent.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchUrgent(pub bool); + +fixed_root_criterion!(TlmMatchUrgent, urgent); + +impl CritFixedRootCriterion for TlmMatchUrgent { + fn matches(&self, data: &ToplevelData) -> bool { + data.wants_attention.get() + } +} diff --git a/src/tree/container.rs b/src/tree/container.rs index fd56767f..06e075ca 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -1152,7 +1152,7 @@ impl ContainerNode { fn mod_attention_requests(&self, set: bool) { let propagate = self.attention_requests.adj(set); if set || propagate { - self.toplevel_data.wants_attention.set(set); + self.toplevel_data.set_wants_attention(set); } if propagate { if let Some(parent) = self.toplevel_data.parent.get() { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index fc5886dc..36f77f14 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -5,7 +5,7 @@ use { CritDestroyListener, CritMatcherId, tlm::{ TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, TL_CHANGED_NEW, - TL_CHANGED_TITLE, TL_CHANGED_VISIBLE, TlMatcherChange, + TL_CHANGED_TITLE, TL_CHANGED_URGENT, TL_CHANGED_VISIBLE, TlMatcherChange, }, }, ifs::{ @@ -647,7 +647,7 @@ impl ToplevelData { if !self.requested_attention.replace(false) { return; } - self.wants_attention.set(false); + self.set_wants_attention(false); if let Some(parent) = self.parent.get() { parent.cnode_child_attention_request_changed(node, false); } @@ -660,12 +660,18 @@ impl ToplevelData { if self.requested_attention.replace(true) { return; } - self.wants_attention.set(true); + self.set_wants_attention(true); if let Some(parent) = self.parent.get() { parent.cnode_child_attention_request_changed(node, true); } } + pub fn set_wants_attention(&self, value: bool) { + if self.wants_attention.replace(value) != value { + self.property_changed(TL_CHANGED_URGENT); + } + } + pub fn output(&self) -> Rc { match self.output_opt() { None => self.state.dummy_output.get().unwrap(), diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index f84a4034..5745898b 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -261,6 +261,7 @@ pub struct WindowMatch { pub app_id_regex: Option, pub floating: Option, pub visible: Option, + pub urgent: 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 1a2b1f3e..69ca4eea 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible), + (app_id, app_id_regex, floating, visible, urgent), ) = ext.extract(( ( opt(str("name")), @@ -74,6 +74,7 @@ impl Parser for WindowMatchParser<'_> { opt(str("app-id-regex")), opt(bol("floating")), opt(bol("visible")), + opt(bol("urgent")), ), ))?; let mut not = None; @@ -121,6 +122,7 @@ impl Parser for WindowMatchParser<'_> { app_id_regex: app_id_regex.despan_into(), floating: floating.despan(), visible: visible.despan(), + urgent: urgent.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 2a7de27a..65062aaa 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -260,6 +260,7 @@ impl Rule for WindowRule { value!(AppIdRegex, app_id_regex); bool!(Floating, floating); bool!(Visible, visible); + bool!(Urgent, urgent); Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 6b21ec88..1a37fd8a 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1803,6 +1803,10 @@ "visible": { "type": "boolean", "description": "Matches if the window is/isn't visible." + }, + "urgent": { + "type": "boolean", + "description": "Matches if the window has/hasn't the urgency flag set." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index cb55fd88..89a1103b 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4046,6 +4046,12 @@ The table has the following fields: The value of this field should be a boolean. +- `urgent` (optional): + + Matches if the window has/hasn't the urgency flag set. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 4bcdbd34..bf817b84 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3491,6 +3491,10 @@ WindowMatch: kind: boolean required: false description: Matches if the window is/isn't visible. + urgent: + kind: boolean + required: false + description: Matches if the window has/hasn't the urgency flag set. WindowMatchExactly: From 91c948b219a132520ef839dd77e8156766d3313c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:43:54 +0200 Subject: [PATCH 24/35] config: add keyboard-focus window criteria --- jay-config/src/_private.rs | 2 ++ jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 3 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 14 ++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + .../tlm/tlm_matchers/tlmm_seat_focus.rs | 28 +++++++++++++++++++ src/ifs/wl_seat/kb_owner.rs | 18 ++++++++++-- src/tree/toplevel.rs | 4 ++- toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 4 ++- toml-config/src/rules.rs | 8 ++++++ toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 6 ++++ toml-spec/spec/spec.yaml | 4 +++ 15 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index bd7bc94a..59f34023 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -6,6 +6,7 @@ pub(crate) mod string_error; use { crate::{ client::ClientMatcher, + input::Seat, video::Mode, window::{WindowMatcher, WindowType}, }, @@ -113,6 +114,7 @@ pub enum WindowCriterionIpc { Floating, Visible, Urgent, + SeatFocus(Seat), } #[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 0881d6bb..cacd9e91 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1660,6 +1660,7 @@ impl ConfigClient { WindowCriterion::Floating => WindowCriterionIpc::Floating, WindowCriterion::Visible => WindowCriterionIpc::Visible, WindowCriterion::Urgent => WindowCriterionIpc::Urgent, + WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), }; 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 0d3e1127..ce7a6918 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -4,6 +4,7 @@ use { crate::{ Axis, Direction, Workspace, client::{Client, ClientCriterion}, + input::Seat, }, serde::{Deserialize, Serialize}, std::ops::Deref, @@ -250,6 +251,8 @@ pub enum WindowCriterion<'a> { Visible, /// Matches if the window has the urgency flag set. Urgent, + /// Matches if the window has the keyboard focus of the seat. + Focus(Seat), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index bdd5fb66..0f675b0d 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2000,6 +2000,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Floating => mgr.floating(), WindowCriterionIpc::Visible => mgr.visible(), WindowCriterionIpc::Urgent => mgr.urgent(), + WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index af9797db..7a2652e6 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -15,11 +15,13 @@ use { tlmm_client::TlmMatchClient, tlmm_floating::TlmMatchFloating, tlmm_kind::TlmMatchKind, + tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, }, + ifs::wl_seat::WlSeatGlobal, state::State, tree::{NodeId, ToplevelData, ToplevelNode}, utils::{ @@ -44,6 +46,7 @@ bitflags! { TL_CHANGED_FLOATING = 1 << 4, TL_CHANGED_VISIBLE = 1 << 5, TL_CHANGED_URGENT = 1 << 6, + TL_CHANGED_SEAT_FOCI = 1 << 7, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -67,6 +70,7 @@ pub struct RootMatchers { clients: CopyHashMap>, title: TlmRootMatcherMap, app_id: TlmRootMatcherMap, + seat_foci: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -129,6 +133,10 @@ impl TlMatcherManager { } } + pub fn has_seat_foci(&self) -> bool { + self.matchers.seat_foci.is_not_empty() + } + pub fn has_no_interest(&self, data: &ToplevelData, change: TlMatcherChange) -> bool { !self.has_interest(data, change) } @@ -175,6 +183,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -244,6 +253,7 @@ impl TlMatcherManager { } conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); + conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -276,6 +286,10 @@ impl TlMatcherManager { pub fn urgent(&self) -> Rc { self.urgent[true].clone() } + + pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { + self.root(TlmMatchSeatFocus::new(seat.id())) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index fcff929e..1d5bd6ec 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -20,6 +20,7 @@ macro_rules! fixed_root_criterion { pub mod tlmm_client; pub mod tlmm_floating; pub mod tlmm_kind; +pub mod tlmm_seat_focus; pub mod tlmm_string; pub mod tlmm_urgent; pub mod tlmm_visible; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs b/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs new file mode 100644 index 00000000..34e75562 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_seat_focus.rs @@ -0,0 +1,28 @@ +use crate::{ + criteria::{ + crit_graph::CritRootCriterion, + tlm::{RootMatchers, TlmRootMatcherMap}, + }, + ifs::wl_seat::SeatId, + tree::ToplevelData, +}; + +pub struct TlmMatchSeatFocus { + id: SeatId, +} + +impl TlmMatchSeatFocus { + pub fn new(id: SeatId) -> TlmMatchSeatFocus { + Self { id } + } +} + +impl CritRootCriterion for TlmMatchSeatFocus { + fn matches(&self, data: &ToplevelData) -> bool { + data.seat_foci.contains(&self.id) + } + + fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap> { + Some(&roots.seat_foci) + } +} diff --git a/src/ifs/wl_seat/kb_owner.rs b/src/ifs/wl_seat/kb_owner.rs index caf7048b..c6950062 100644 --- a/src/ifs/wl_seat/kb_owner.rs +++ b/src/ifs/wl_seat/kb_owner.rs @@ -1,7 +1,7 @@ use { crate::{ - ifs::wl_seat::WlSeatGlobal, tree::Node, utils::clonecell::CloneCell, - xwayland::XWaylandEvent, + criteria::tlm::TL_CHANGED_SEAT_FOCI, ifs::wl_seat::WlSeatGlobal, tree::Node, + utils::clonecell::CloneCell, xwayland::XWaylandEvent, }, std::rc::Rc, }; @@ -61,6 +61,18 @@ impl KbOwner for DefaultKbOwner { } fn set_kb_node(&self, seat: &Rc, node: Rc, serial: u64) { + macro_rules! notify_matcher { + ($node:expr, $data:ident, $block:expr) => { + if let Some(tl) = $node.clone().node_toplevel() { + let $data = tl.tl_data(); + $block; + if seat.state.tl_matcher_manager.has_seat_foci() { + $data.property_changed(TL_CHANGED_SEAT_FOCI); + } + } + }; + } + let old = seat.keyboard_node.get(); if old.node_id() == node.node_id() { return; @@ -70,6 +82,7 @@ impl KbOwner for DefaultKbOwner { seat.state.xwayland.queue.push(XWaylandEvent::ActivateRoot); } old.node_on_unfocus(seat); + notify_matcher!(old, data, data.seat_foci.remove(&seat.id)); if old.node_seat_state().unfocus(seat) { old.node_active_changed(false); } @@ -79,6 +92,7 @@ impl KbOwner for DefaultKbOwner { } // log::info!("focus {}", node.node_id()); node.clone().node_on_focus(seat); + notify_matcher!(node, data, data.seat_foci.set(seat.id, ())); seat.keyboard_node_serial.set(serial); seat.keyboard_node.set(node.clone()); seat.tablet_on_keyboard_node_change(); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 36f77f14..afcf0e02 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -14,7 +14,7 @@ use { ext_image_copy::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, jay_screencast::JayScreencast, jay_toplevel::JayToplevel, - wl_seat::{NodeSeatState, collect_kb_foci, collect_kb_foci2}, + wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, wl_surface::WlSurface, }, rect::Rect, @@ -324,6 +324,7 @@ pub struct ToplevelData { pub slf: Weak, pub destroyed: CopyHashMap>>, pub changed_properties: Cell, + pub seat_foci: CopyHashMap, } impl ToplevelData { @@ -370,6 +371,7 @@ impl ToplevelData { slf: slf.clone(), destroyed: Default::default(), changed_properties: Default::default(), + seat_foci: Default::default(), } } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 5745898b..dedc5049 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -262,6 +262,7 @@ pub struct WindowMatch { pub floating: Option, pub visible: Option, pub urgent: Option, + pub focused: 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 69ca4eea..5f66362a 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible, urgent), + (app_id, app_id_regex, floating, visible, urgent, focused), ) = ext.extract(( ( opt(str("name")), @@ -75,6 +75,7 @@ impl Parser for WindowMatchParser<'_> { opt(bol("floating")), opt(bol("visible")), opt(bol("urgent")), + opt(bol("focused")), ), ))?; let mut not = None; @@ -123,6 +124,7 @@ impl Parser for WindowMatchParser<'_> { floating: floating.despan(), visible: visible.despan(), urgent: urgent.despan(), + focused: focused.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 65062aaa..a37a0ab5 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -261,6 +261,14 @@ impl Rule for WindowRule { bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); + if let Some(value) = match_.focused { + let crit = WindowCriterion::Focus(state.persistent.seat); + let matcher = match value { + false => m(WindowCriterion::Not(&crit)), + true => m(crit), + }; + all.push(matcher); + } Some(()) } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1a37fd8a..953900aa 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1807,6 +1807,10 @@ "urgent": { "type": "boolean", "description": "Matches if the window has/hasn't the urgency flag set." + }, + "focused": { + "type": "boolean", + "description": "Matches if the window has/hasn't the keyboard focus." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 89a1103b..058d2eda 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4052,6 +4052,12 @@ The table has the following fields: The value of this field should be a boolean. +- `focused` (optional): + + Matches if the window has/hasn't the keyboard focus. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index bf817b84..3f88b423 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3495,6 +3495,10 @@ WindowMatch: kind: boolean required: false description: Matches if the window has/hasn't the urgency flag set. + focused: + kind: boolean + required: false + description: Matches if the window has/hasn't the keyboard focus. WindowMatchExactly: From e36ccd560ce32a9b8c7f086939343279106e5e64 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:49:39 +0200 Subject: [PATCH 25/35] config: add fullscreen window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 2 ++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 10 ++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + src/criteria/tlm/tlm_matchers/tlmm_fullscreen.rs | 11 +++++++++++ src/tree/toplevel.rs | 7 +++++-- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/window_match.rs | 4 +++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 ++++ toml-spec/spec/spec.generated.md | 6 ++++++ toml-spec/spec/spec.yaml | 4 ++++ 14 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_fullscreen.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 59f34023..e7303d20 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -115,6 +115,7 @@ pub enum WindowCriterionIpc { Visible, Urgent, SeatFocus(Seat), + Fullscreen, } #[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 cacd9e91..09660de4 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1661,6 +1661,7 @@ impl ConfigClient { WindowCriterion::Visible => WindowCriterionIpc::Visible, WindowCriterion::Urgent => WindowCriterionIpc::Urgent, WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), + WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, }; 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 ce7a6918..cb105c0b 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -253,6 +253,8 @@ pub enum WindowCriterion<'a> { Urgent, /// Matches if the window has the keyboard focus of the seat. Focus(Seat), + /// Matches if the window is fullscreen. + Fullscreen, } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 0f675b0d..62c957a1 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2001,6 +2001,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Visible => mgr.visible(), WindowCriterionIpc::Urgent => mgr.urgent(), WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), + WindowCriterionIpc::Fullscreen => mgr.fullscreen(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 7a2652e6..658444b1 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -14,6 +14,7 @@ use { tlm::tlm_matchers::{ tlmm_client::TlmMatchClient, tlmm_floating::TlmMatchFloating, + tlmm_fullscreen::TlmMatchFullscreen, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, @@ -47,6 +48,7 @@ bitflags! { TL_CHANGED_VISIBLE = 1 << 5, TL_CHANGED_URGENT = 1 << 6, TL_CHANGED_SEAT_FOCI = 1 << 7, + TL_CHANGED_FULLSCREEN = 1 << 8, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -59,6 +61,7 @@ pub struct TlMatcherManager { floating: TlmFixedRootMatcher, visible: TlmFixedRootMatcher, urgent: TlmFixedRootMatcher, + fullscreen: TlmFixedRootMatcher, matchers: Rc, } @@ -113,6 +116,7 @@ impl TlMatcherManager { floating: bool!(TlmMatchFloating), visible: bool!(TlmMatchVisible), urgent: bool!(TlmMatchUrgent), + fullscreen: bool!(TlmMatchFullscreen), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), @@ -187,6 +191,7 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); + fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); false } @@ -257,6 +262,7 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); + fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -283,6 +289,10 @@ impl TlMatcherManager { self.visible[true].clone() } + pub fn fullscreen(&self) -> Rc { + self.fullscreen[true].clone() + } + pub fn urgent(&self) -> Rc { self.urgent[true].clone() } diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 1d5bd6ec..550fc739 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -19,6 +19,7 @@ macro_rules! fixed_root_criterion { pub mod tlmm_client; pub mod tlmm_floating; +pub mod tlmm_fullscreen; pub mod tlmm_kind; pub mod tlmm_seat_focus; pub mod tlmm_string; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_fullscreen.rs b/src/criteria/tlm/tlm_matchers/tlmm_fullscreen.rs new file mode 100644 index 00000000..5ed21689 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_fullscreen.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchFullscreen(pub bool); + +fixed_root_criterion!(TlmMatchFullscreen, fullscreen); + +impl CritFixedRootCriterion for TlmMatchFullscreen { + fn matches(&self, data: &ToplevelData) -> bool { + data.is_fullscreen.get() + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index afcf0e02..bab92811 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -4,8 +4,9 @@ use { criteria::{ CritDestroyListener, CritMatcherId, tlm::{ - TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING, TL_CHANGED_NEW, - TL_CHANGED_TITLE, TL_CHANGED_URGENT, TL_CHANGED_VISIBLE, TlMatcherChange, + 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, TlMatcherChange, }, }, ifs::{ @@ -579,6 +580,7 @@ impl ToplevelData { }); drop(data); self.is_fullscreen.set(true); + self.property_changed(TL_CHANGED_FULLSCREEN); node.tl_set_parent(ws.clone()); ws.set_fullscreen_node(&node); node.clone() @@ -601,6 +603,7 @@ impl ToplevelData { } }; self.is_fullscreen.set(false); + self.property_changed(TL_CHANGED_FULLSCREEN); match fd.workspace.fullscreen.get() { None => { log::error!( diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index dedc5049..34e54f75 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -263,6 +263,7 @@ pub struct WindowMatch { pub visible: Option, pub urgent: Option, pub focused: Option, + pub fullscreen: 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 5f66362a..f2a51571 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible, urgent, focused), + (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen), ) = ext.extract(( ( opt(str("name")), @@ -76,6 +76,7 @@ impl Parser for WindowMatchParser<'_> { opt(bol("visible")), opt(bol("urgent")), opt(bol("focused")), + opt(bol("fullscreen")), ), ))?; let mut not = None; @@ -125,6 +126,7 @@ impl Parser for WindowMatchParser<'_> { visible: visible.despan(), urgent: urgent.despan(), focused: focused.despan(), + fullscreen: fullscreen.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index a37a0ab5..31f40a94 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -261,6 +261,7 @@ impl Rule for WindowRule { bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); + bool!(Fullscreen, fullscreen); if let Some(value) = match_.focused { let crit = WindowCriterion::Focus(state.persistent.seat); let matcher = match value { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 953900aa..fad46f11 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1811,6 +1811,10 @@ "focused": { "type": "boolean", "description": "Matches if the window has/hasn't the keyboard focus." + }, + "fullscreen": { + "type": "boolean", + "description": "Matches if the window is/isn't fullscreen." } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 058d2eda..66a42572 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4058,6 +4058,12 @@ The table has the following fields: The value of this field should be a boolean. +- `fullscreen` (optional): + + Matches if the window is/isn't fullscreen. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 3f88b423..98c8f37d 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3499,6 +3499,10 @@ WindowMatch: kind: boolean required: false description: Matches if the window has/hasn't the keyboard focus. + fullscreen: + kind: boolean + required: false + description: Matches if the window is/isn't fullscreen. WindowMatchExactly: From 5f1268cada395c4e9b7b429d9d83940a122c3fdf Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 18:52:55 +0200 Subject: [PATCH 26/35] config: add just-mapped window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 1 + jay-config/src/window.rs | 5 ++++ src/async_engine.rs | 4 +-- src/compositor.rs | 9 +++++- src/config/handler.rs | 1 + src/criteria/tlm.rs | 30 +++++++++++++++++++ src/criteria/tlm/tlm_matchers.rs | 1 + .../tlm/tlm_matchers/tlmm_just_mapped.rs | 11 +++++++ src/tree/toplevel.rs | 9 ++++++ toml-config/src/config.rs | 1 + .../src/config/parsers/window_match.rs | 4 ++- toml-config/src/rules.rs | 1 + toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 9 ++++++ toml-spec/spec/spec.yaml | 8 +++++ 16 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index e7303d20..62ff5b09 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -116,6 +116,7 @@ pub enum WindowCriterionIpc { Urgent, SeatFocus(Seat), Fullscreen, + JustMapped, } #[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 09660de4..6bdd2f1e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1662,6 +1662,7 @@ impl ConfigClient { WindowCriterion::Urgent => WindowCriterionIpc::Urgent, WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, + WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, }; 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 cb105c0b..a5ef9036 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -255,6 +255,11 @@ pub enum WindowCriterion<'a> { Focus(Seat), /// Matches if the window is fullscreen. Fullscreen, + /// Matches if the window has/hasn't just been mapped. + /// + /// This is true for one iteration of the compositor's main loop immediately after the + /// window has been mapped. + JustMapped, } impl WindowCriterion<'_> { diff --git a/src/async_engine.rs b/src/async_engine.rs index e936dd46..80e38c52 100644 --- a/src/async_engine.rs +++ b/src/async_engine.rs @@ -105,7 +105,6 @@ impl AsyncEngine { break; } self.now.take(); - self.iteration.fetch_add(1); let mut phase = 0; while phase < NUM_PHASES { self.queues[phase].swap(&mut *stash); @@ -121,6 +120,7 @@ impl AsyncEngine { } } } + self.iteration.fetch_add(1); self.yields.swap(&mut *yield_stash); while let Some(waker) = yield_stash.pop_front() { waker.wake(); @@ -153,7 +153,7 @@ impl AsyncEngine { self.yields.push(waker); } - fn iteration(&self) -> u64 { + pub fn iteration(&self) -> u64 { self.iteration.get() } diff --git a/src/compositor.rs b/src/compositor.rs index 2c3352ee..82967771 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -18,7 +18,9 @@ use { criteria::{ CritMatcherIds, clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events}, - tlm::{TlMatcherManager, handle_tl_changes, handle_tl_leaf_events}, + tlm::{ + TlMatcherManager, handle_tl_changes, handle_tl_just_mapped, handle_tl_leaf_events, + }, }, damage::{DamageVisualizer, visualize_damage}, dbus::Dbus, @@ -484,6 +486,11 @@ fn start_global_event_handlers( "tl matcher leaf events", handle_tl_leaf_events(state.clone()), ), + eng.spawn2( + "tl matcher just mapped", + Phase::Layout, + handle_tl_just_mapped(state.clone()), + ), ] } diff --git a/src/config/handler.rs b/src/config/handler.rs index 62c957a1..2241dc08 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2002,6 +2002,7 @@ impl ConfigProxyHandler { WindowCriterionIpc::Urgent => mgr.urgent(), WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), WindowCriterionIpc::Fullscreen => mgr.fullscreen(), + WindowCriterionIpc::JustMapped => mgr.just_mapped(), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 658444b1..75b6fa08 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -15,6 +15,7 @@ use { tlmm_client::TlmMatchClient, tlmm_floating::TlmMatchFloating, tlmm_fullscreen::TlmMatchFullscreen, + tlmm_just_mapped::TlmMatchJustMapped, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{TlmMatchAppId, TlmMatchTitle}, @@ -49,6 +50,7 @@ bitflags! { TL_CHANGED_URGENT = 1 << 6, TL_CHANGED_SEAT_FOCI = 1 << 7, TL_CHANGED_FULLSCREEN = 1 << 8, + TL_CHANGED_JUST_MAPPED = 1 << 9, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -57,11 +59,13 @@ pub struct TlMatcherManager { ids: Rc, changes: AsyncQueue>, leaf_events: Rc>>, + handle_just_mapped: AsyncQueue>, constant: TlmFixedRootMatcher>, floating: TlmFixedRootMatcher, visible: TlmFixedRootMatcher, urgent: TlmFixedRootMatcher, fullscreen: TlmFixedRootMatcher, + just_mapped: TlmFixedRootMatcher, matchers: Rc, } @@ -94,6 +98,16 @@ pub async fn handle_tl_leaf_events(state: Rc) { } } +pub async fn handle_tl_just_mapped(state: Rc) { + let mgr = &state.tl_matcher_manager; + loop { + let tl = mgr.handle_just_mapped.pop().await; + let data = tl.tl_data(); + data.just_mapped_scheduled.set(false); + data.property_changed(TL_CHANGED_JUST_MAPPED); + } +} + pub type TlmUpstreamNode = dyn CritUpstreamNode; pub type TlmLeafMatcher = CritLeafMatcher; @@ -117,16 +131,19 @@ impl TlMatcherManager { visible: bool!(TlmMatchVisible), urgent: bool!(TlmMatchUrgent), fullscreen: bool!(TlmMatchFullscreen), + just_mapped: bool!(TlmMatchJustMapped), changes: Default::default(), leaf_events: Default::default(), ids: ids.clone(), matchers, + handle_just_mapped: Default::default(), } } pub fn clear(&self) { self.changes.clear(); self.leaf_events.clear(); + self.handle_just_mapped.clear(); } pub fn rematch_all(&self, state: &Rc) { @@ -192,6 +209,7 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); + fixed_conditional!(TL_CHANGED_JUST_MAPPED, just_mapped); false } @@ -263,6 +281,14 @@ impl TlMatcherManager { fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen); + fixed_conditional!(TL_CHANGED_JUST_MAPPED, just_mapped); + if changed.contains(TL_CHANGED_JUST_MAPPED) + && data.just_mapped() + && (self.just_mapped[false].has_downstream() || self.just_mapped[true].has_downstream()) + && !data.just_mapped_scheduled.replace(true) + { + self.handle_just_mapped.push(node); + } } pub fn title(&self, string: CritLiteralOrRegex) -> Rc { @@ -297,6 +323,10 @@ impl TlMatcherManager { self.urgent[true].clone() } + pub fn just_mapped(&self) -> Rc { + self.just_mapped[true].clone() + } + pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { self.root(TlmMatchSeatFocus::new(seat.id())) } diff --git a/src/criteria/tlm/tlm_matchers.rs b/src/criteria/tlm/tlm_matchers.rs index 550fc739..6d900774 100644 --- a/src/criteria/tlm/tlm_matchers.rs +++ b/src/criteria/tlm/tlm_matchers.rs @@ -20,6 +20,7 @@ macro_rules! fixed_root_criterion { pub mod tlmm_client; pub mod tlmm_floating; pub mod tlmm_fullscreen; +pub mod tlmm_just_mapped; pub mod tlmm_kind; pub mod tlmm_seat_focus; pub mod tlmm_string; diff --git a/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs b/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs new file mode 100644 index 00000000..e21b5675 --- /dev/null +++ b/src/criteria/tlm/tlm_matchers/tlmm_just_mapped.rs @@ -0,0 +1,11 @@ +use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData}; + +pub struct TlmMatchJustMapped(pub bool); + +fixed_root_criterion!(TlmMatchJustMapped, just_mapped); + +impl CritFixedRootCriterion for TlmMatchJustMapped { + fn matches(&self, data: &ToplevelData) -> bool { + data.just_mapped() + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index bab92811..d328b2ee 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -103,6 +103,7 @@ impl ToplevelNode for T { let data = self.tl_data(); let parent_was_none = data.parent.set(Some(parent.clone())).is_none(); if parent_was_none { + data.mapped_during_iteration.set(data.state.eng.iteration()); data.property_changed(TL_CHANGED_NEW); } let was_floating = data.is_floating.get(); @@ -308,6 +309,7 @@ pub struct ToplevelData { pub workspace: CloneCell>>, pub title: RefCell, pub parent: CloneCell>>, + pub mapped_during_iteration: Cell, pub pos: Cell, pub desired_extents: Cell, pub seat_state: NodeSeatState, @@ -325,6 +327,7 @@ pub struct ToplevelData { pub slf: Weak, pub destroyed: CopyHashMap>>, pub changed_properties: Cell, + pub just_mapped_scheduled: Cell, pub seat_foci: CopyHashMap, } @@ -357,6 +360,7 @@ impl ToplevelData { workspace: Default::default(), title: RefCell::new(title), parent: Default::default(), + mapped_during_iteration: Cell::new(0), pos: Default::default(), desired_extents: Default::default(), seat_state: Default::default(), @@ -372,6 +376,7 @@ impl ToplevelData { slf: slf.clone(), destroyed: Default::default(), changed_properties: Default::default(), + just_mapped_scheduled: Cell::new(false), seat_foci: Default::default(), } } @@ -696,6 +701,10 @@ impl ToplevelData { }; (0, 0) } + + pub fn just_mapped(&self) -> bool { + self.mapped_during_iteration.get() == self.state.eng.iteration() + } } impl Drop for ToplevelData { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 34e54f75..dc35cbce 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -264,6 +264,7 @@ pub struct WindowMatch { pub urgent: Option, pub focused: Option, pub fullscreen: Option, + pub just_mapped: 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 f2a51571..864e897c 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,7 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen), + (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen, just_mapped), ) = ext.extract(( ( opt(str("name")), @@ -77,6 +77,7 @@ impl Parser for WindowMatchParser<'_> { opt(bol("urgent")), opt(bol("focused")), opt(bol("fullscreen")), + opt(bol("just-mapped")), ), ))?; let mut not = None; @@ -127,6 +128,7 @@ impl Parser for WindowMatchParser<'_> { urgent: urgent.despan(), focused: focused.despan(), fullscreen: fullscreen.despan(), + just_mapped: just_mapped.despan(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 31f40a94..42c75b24 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -262,6 +262,7 @@ impl Rule for WindowRule { bool!(Visible, visible); bool!(Urgent, urgent); bool!(Fullscreen, fullscreen); + bool!(JustMapped, just_mapped); if let Some(value) = match_.focused { let crit = WindowCriterion::Focus(state.persistent.seat); let matcher = match value { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index fad46f11..7413ab70 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1815,6 +1815,10 @@ "fullscreen": { "type": "boolean", "description": "Matches if the window is/isn't fullscreen." + }, + "just-mapped": { + "type": "boolean", + "description": "Matches if the window has/hasn't just been mapped.\n\nThis is true for one iteration of the compositor's main loop immediately after the\nwindow has been mapped.\n" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 66a42572..b8c5192d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4064,6 +4064,15 @@ The table has the following fields: The value of this field should be a boolean. +- `just-mapped` (optional): + + Matches if the window has/hasn't just been mapped. + + This is true for one iteration of the compositor's main loop immediately after the + window has been mapped. + + The value of this field should be a boolean. + ### `WindowMatchExactly` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 98c8f37d..79b7294d 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3503,6 +3503,14 @@ WindowMatch: kind: boolean required: false description: Matches if the window is/isn't fullscreen. + just-mapped: + kind: boolean + required: false + description: | + Matches if the window has/hasn't just been mapped. + + This is true for one iteration of the compositor's main loop immediately after the + window has been mapped. WindowMatchExactly: From 6d3d4dcabb9acc334443c5ad6d9df75dce92879b Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Thu, 1 May 2025 17:31:42 +0200 Subject: [PATCH 27/35] config: add toplevel-tag window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 2 ++ jay-config/src/window.rs | 4 ++++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 10 +++++++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 17 ++++++++++++++++- src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs | 12 +++++++++++- src/ifs/xdg_toplevel_tag_manager_v1.rs | 12 +++++++++++- src/tree/toplevel.rs | 6 +++--- toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/window_match.rs | 17 ++++++++++++++++- 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 ++++++++ 15 files changed, 106 insertions(+), 8 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 62ff5b09..881493e5 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -123,4 +123,5 @@ pub enum WindowCriterionIpc { pub enum WindowCriterionStringField { Title, AppId, + Tag, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 6bdd2f1e..68fc1678 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1663,6 +1663,8 @@ impl ConfigClient { WindowCriterion::Focus(seat) => WindowCriterionIpc::SeatFocus(seat), WindowCriterion::Fullscreen => WindowCriterionIpc::Fullscreen, WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, + WindowCriterion::Tag(t) => string!(t, Tag, false), + WindowCriterion::TagRegex(t) => string!(t, Tag, 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 a5ef9036..3ee6bef9 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -260,6 +260,10 @@ pub enum WindowCriterion<'a> { /// This is true for one iteration of the compositor's main loop immediately after the /// window has been mapped. JustMapped, + /// Matches the toplevel-tag of the window verbatim. + Tag(&'a str), + /// Matches the toplevel-tag of the window with a regular expression. + TagRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 2241dc08..0f17561f 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1990,6 +1990,7 @@ impl ConfigProxyHandler { match *field { WindowCriterionStringField::Title => mgr.title(needle), WindowCriterionStringField::AppId => mgr.app_id(needle), + WindowCriterionStringField::Tag => mgr.tag(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 75b6fa08..042e245f 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -18,7 +18,7 @@ use { tlmm_just_mapped::TlmMatchJustMapped, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, - tlmm_string::{TlmMatchAppId, TlmMatchTitle}, + tlmm_string::{TlmMatchAppId, TlmMatchTag, TlmMatchTitle}, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, @@ -51,6 +51,7 @@ bitflags! { TL_CHANGED_SEAT_FOCI = 1 << 7, TL_CHANGED_FULLSCREEN = 1 << 8, TL_CHANGED_JUST_MAPPED = 1 << 9, + TL_CHANGED_TAG = 1 << 10, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -76,6 +77,7 @@ pub struct RootMatchers { kinds: TlmRootMatcherMap, clients: CopyHashMap>, title: TlmRootMatcherMap, + tag: TlmRootMatcherMap, app_id: TlmRootMatcherMap, seat_foci: TlmRootMatcherMap, } @@ -205,6 +207,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); + conditional!(TL_CHANGED_TAG, tag); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -277,6 +280,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TITLE, title); conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); + conditional!(TL_CHANGED_TAG, tag); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -299,6 +303,10 @@ impl TlMatcherManager { self.root(TlmMatchAppId::new(string)) } + pub fn tag(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchTag::new(string)) + } + pub fn floating(&self) -> Rc { self.floating[true].clone() } diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index 967d8032..352c08b4 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -3,16 +3,18 @@ use crate::{ crit_matchers::critm_string::{CritMatchString, StringAccess}, tlm::{RootMatchers, TlmRootMatcherMap}, }, - tree::ToplevelData, + tree::{ToplevelData, ToplevelType}, }; pub type TlmMatchString = CritMatchString; pub type TlmMatchTitle = TlmMatchString; pub type TlmMatchAppId = TlmMatchString; +pub type TlmMatchTag = TlmMatchString; pub struct TitleAccess; pub struct AppIdAccess; +pub struct TagAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -33,3 +35,16 @@ impl StringAccess for AppIdAccess { &roots.app_id } } + +impl StringAccess for TagAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XdgToplevel(data) = &data.kind { + return f(&data.tag.borrow()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.tag + } +} diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 0931ff8d..9bdc3d3c 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -92,6 +92,11 @@ pub enum Decoration { Server, } +#[derive(Debug)] +pub struct XdgToplevelToplevelData { + pub tag: RefCell, +} + pub struct XdgToplevel { pub id: XdgToplevelId, pub state: Rc, @@ -112,6 +117,7 @@ pub struct XdgToplevel { is_mapped: Cell, dialog: CloneCell>>, extents_set: Cell, + pub data: Rc, } impl Debug for XdgToplevel { @@ -135,6 +141,9 @@ impl XdgToplevel { } let state = &surface.surface.client.state; let node_id = state.node_ids.next(); + let data = Rc::new(XdgToplevelToplevelData { + tag: Default::default(), + }); Self { id, state: state.clone(), @@ -154,7 +163,7 @@ impl XdgToplevel { state, String::new(), Some(surface.surface.client.clone()), - ToplevelType::XdgToplevel, + ToplevelType::XdgToplevel(data.clone()), node_id, slf, ), @@ -162,6 +171,7 @@ impl XdgToplevel { is_mapped: Cell::new(false), dialog: Default::default(), extents_set: Cell::new(false), + data, } } diff --git a/src/ifs/xdg_toplevel_tag_manager_v1.rs b/src/ifs/xdg_toplevel_tag_manager_v1.rs index 69fc3db1..cdcecdab 100644 --- a/src/ifs/xdg_toplevel_tag_manager_v1.rs +++ b/src/ifs/xdg_toplevel_tag_manager_v1.rs @@ -1,9 +1,11 @@ use { crate::{ client::{Client, ClientError}, + criteria::tlm::TL_CHANGED_TAG, globals::{Global, GlobalName}, leaks::Tracker, object::{Object, Version}, + tree::ToplevelNodeBase, wire::{XdgToplevelTagManagerV1Id, xdg_toplevel_tag_manager_v1::*}, }, std::rc::Rc, @@ -72,9 +74,17 @@ impl XdgToplevelTagManagerV1RequestHandler for XdgToplevelTagManagerV1 { fn set_toplevel_tag( &self, - _req: SetToplevelTag<'_>, + req: SetToplevelTag<'_>, _slf: &Rc, ) -> Result<(), Self::Error> { + let tl = self.client.lookup(req.toplevel)?; + let tag = &mut *tl.data.tag.borrow_mut(); + if tag == req.tag { + return Ok(()); + } + tag.clear(); + tag.push_str(req.tag); + tl.tl_data().property_changed(TL_CHANGED_TAG); Ok(()) } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index d328b2ee..dd8daa83 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -16,7 +16,7 @@ use { jay_screencast::JayScreencast, jay_toplevel::JayToplevel, wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, - wl_surface::WlSurface, + wl_surface::{WlSurface, xdg_surface::xdg_toplevel::XdgToplevelToplevelData}, }, rect::Rect, state::State, @@ -277,7 +277,7 @@ impl ToplevelOpt { pub enum ToplevelType { Container, Placeholder, - XdgToplevel, + XdgToplevel(Rc), XWindow, } @@ -286,7 +286,7 @@ impl ToplevelType { match self { ToplevelType::Container => window::CONTAINER, ToplevelType::Placeholder => window::PLACEHOLDER, - ToplevelType::XdgToplevel => window::XDG_TOPLEVEL, + ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, ToplevelType::XWindow => window::X_WINDOW, } } diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index dc35cbce..bd95a0e3 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -265,6 +265,8 @@ pub struct WindowMatch { pub focused: Option, pub fullscreen: Option, pub just_mapped: Option, + pub tag: Option, + pub tag_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 864e897c..f0f3eba8 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -56,7 +56,18 @@ impl Parser for WindowMatchParser<'_> { title, title_regex, ), - (app_id, app_id_regex, floating, visible, urgent, focused, fullscreen, just_mapped), + ( + app_id, + app_id_regex, + floating, + visible, + urgent, + focused, + fullscreen, + just_mapped, + tag, + tag_regex, + ), ) = ext.extract(( ( opt(str("name")), @@ -78,6 +89,8 @@ impl Parser for WindowMatchParser<'_> { opt(bol("focused")), opt(bol("fullscreen")), opt(bol("just-mapped")), + opt(str("tag")), + opt(str("tag-regex")), ), ))?; let mut not = None; @@ -129,6 +142,8 @@ impl Parser for WindowMatchParser<'_> { focused: focused.despan(), fullscreen: fullscreen.despan(), just_mapped: just_mapped.despan(), + tag: tag.despan_into(), + tag_regex: tag_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 42c75b24..c58f0f26 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -258,6 +258,8 @@ impl Rule for WindowRule { value!(TitleRegex, title_regex); value!(AppId, app_id); value!(AppIdRegex, app_id_regex); + value!(Tag, tag); + value!(TagRegex, tag_regex); bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 7413ab70..1bb7c8a8 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1819,6 +1819,14 @@ "just-mapped": { "type": "boolean", "description": "Matches if the window has/hasn't just been mapped.\n\nThis is true for one iteration of the compositor's main loop immediately after the\nwindow has been mapped.\n" + }, + "tag": { + "type": "string", + "description": "Matches the toplevel-tag of the window verbatim." + }, + "tag-regex": { + "type": "string", + "description": "Matches the toplevel-tag 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 b8c5192d..e5f0e8a4 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4073,6 +4073,18 @@ The table has the following fields: The value of this field should be a boolean. +- `tag` (optional): + + Matches the toplevel-tag of the window verbatim. + + The value of this field should be a string. + +- `tag-regex` (optional): + + Matches the toplevel-tag 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 79b7294d..134dba12 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3511,6 +3511,14 @@ WindowMatch: This is true for one iteration of the compositor's main loop immediately after the window has been mapped. + tag: + kind: string + required: false + description: Matches the toplevel-tag of the window verbatim. + tag-regex: + kind: string + required: false + description: Matches the toplevel-tag of the window with a regular expression. WindowMatchExactly: From faa0b27ef8003ce550d6708d724e7fd702a9b377 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 13:37:23 +0200 Subject: [PATCH 28/35] config: add WM_CLASS window criteria --- jay-config/src/_private.rs | 2 ++ jay-config/src/_private/client.rs | 4 +++ jay-config/src/window.rs | 8 +++++ src/config/handler.rs | 2 ++ src/criteria/tlm.rs | 19 +++++++++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 30 +++++++++++++++++++ src/ifs/wl_surface/x_surface/xwindow.rs | 6 ++-- src/tree/toplevel.rs | 10 ++++--- src/xwayland/xwm.rs | 13 ++++++-- toml-config/src/config.rs | 4 +++ .../src/config/parsers/window_match.rs | 11 +++++++ toml-config/src/rules.rs | 4 +++ toml-spec/spec/spec.generated.json | 16 ++++++++++ toml-spec/spec/spec.generated.md | 24 +++++++++++++++ toml-spec/spec/spec.yaml | 16 ++++++++++ 15 files changed, 159 insertions(+), 10 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 881493e5..6d666c2f 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -124,4 +124,6 @@ pub enum WindowCriterionStringField { Title, AppId, Tag, + XClass, + XInstance, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 68fc1678..14cd901b 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1665,6 +1665,10 @@ impl ConfigClient { WindowCriterion::JustMapped => WindowCriterionIpc::JustMapped, WindowCriterion::Tag(t) => string!(t, Tag, false), WindowCriterion::TagRegex(t) => string!(t, Tag, true), + WindowCriterion::XClass(t) => string!(t, XClass, false), + WindowCriterion::XClassRegex(t) => string!(t, XClass, true), + WindowCriterion::XInstance(t) => string!(t, XInstance, false), + WindowCriterion::XInstanceRegex(t) => string!(t, XInstance, 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 3ee6bef9..e2438e21 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -264,6 +264,14 @@ pub enum WindowCriterion<'a> { Tag(&'a str), /// Matches the toplevel-tag of the window with a regular expression. TagRegex(&'a str), + /// Matches the X class of the window verbatim. + XClass(&'a str), + /// Matches the X class of the window with a regular expression. + XClassRegex(&'a str), + /// Matches the X instance of the window verbatim. + XInstance(&'a str), + /// Matches the X instance of the window with a regular expression. + XInstanceRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 0f17561f..6b9660f0 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1991,6 +1991,8 @@ impl ConfigProxyHandler { WindowCriterionStringField::Title => mgr.title(needle), WindowCriterionStringField::AppId => mgr.app_id(needle), WindowCriterionStringField::Tag => mgr.tag(needle), + WindowCriterionStringField::XClass => mgr.class(needle), + WindowCriterionStringField::XInstance => mgr.instance(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index 042e245f..e58a51ed 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -18,7 +18,9 @@ use { tlmm_just_mapped::TlmMatchJustMapped, tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, - tlmm_string::{TlmMatchAppId, TlmMatchTag, TlmMatchTitle}, + tlmm_string::{ + TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchTag, TlmMatchTitle, + }, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, }, @@ -52,6 +54,7 @@ bitflags! { TL_CHANGED_FULLSCREEN = 1 << 8, TL_CHANGED_JUST_MAPPED = 1 << 9, TL_CHANGED_TAG = 1 << 10, + TL_CHANGED_CLASS_INST = 1 << 11, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -80,6 +83,8 @@ pub struct RootMatchers { tag: TlmRootMatcherMap, app_id: TlmRootMatcherMap, seat_foci: TlmRootMatcherMap, + class: TlmRootMatcherMap, + instance: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -208,6 +213,8 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_TAG, tag); + conditional!(TL_CHANGED_CLASS_INST, class); + conditional!(TL_CHANGED_CLASS_INST, instance); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -281,6 +288,8 @@ impl TlMatcherManager { conditional!(TL_CHANGED_APP_ID, app_id); conditional!(TL_CHANGED_SEAT_FOCI, seat_foci); conditional!(TL_CHANGED_TAG, tag); + conditional!(TL_CHANGED_CLASS_INST, class); + conditional!(TL_CHANGED_CLASS_INST, instance); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -338,6 +347,14 @@ impl TlMatcherManager { pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc { self.root(TlmMatchSeatFocus::new(seat.id())) } + + pub fn class(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchClass::new(string)) + } + + pub fn instance(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchInstance::new(string)) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index 352c08b4..6800ecdf 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -11,10 +11,14 @@ pub type TlmMatchString = CritMatchString; pub type TlmMatchTitle = TlmMatchString; pub type TlmMatchAppId = TlmMatchString; pub type TlmMatchTag = TlmMatchString; +pub type TlmMatchClass = TlmMatchString; +pub type TlmMatchInstance = TlmMatchString; pub struct TitleAccess; pub struct AppIdAccess; pub struct TagAccess; +pub struct ClassAccess; +pub struct InstanceAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -48,3 +52,29 @@ impl StringAccess for TagAccess { &roots.tag } } + +impl StringAccess for ClassAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XWindow(data) = &data.kind { + return f(&data.info.class.borrow().as_deref().unwrap_or_default()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.class + } +} + +impl StringAccess for InstanceAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XWindow(data) = &data.kind { + return f(&data.info.instance.borrow().as_deref().unwrap_or_default()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.instance + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index b6ca97be..e0f497be 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -90,8 +90,8 @@ pub struct XwindowInfo { pub override_redirect: Cell, pub extents: Cell, pub pending_extents: Cell, - pub instance: RefCell>, - pub class: RefCell>, + pub instance: RefCell>, + pub class: RefCell>, pub title: RefCell>, pub role: RefCell>, pub protocols: CopyHashMap, @@ -211,7 +211,7 @@ impl Xwindow { &data.state, data.info.title.borrow_mut().clone().unwrap_or_default(), Some(surface.client.clone()), - ToplevelType::XWindow, + ToplevelType::XWindow(data.clone()), id, weak, ); diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index dd8daa83..6992aac6 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -16,7 +16,10 @@ use { jay_screencast::JayScreencast, jay_toplevel::JayToplevel, wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2}, - wl_surface::{WlSurface, xdg_surface::xdg_toplevel::XdgToplevelToplevelData}, + wl_surface::{ + WlSurface, x_surface::xwindow::XwindowData, + xdg_surface::xdg_toplevel::XdgToplevelToplevelData, + }, }, rect::Rect, state::State, @@ -273,12 +276,11 @@ impl ToplevelOpt { } } -#[derive(Debug)] pub enum ToplevelType { Container, Placeholder, XdgToplevel(Rc), - XWindow, + XWindow(Rc), } impl ToplevelType { @@ -287,7 +289,7 @@ impl ToplevelType { ToplevelType::Container => window::CONTAINER, ToplevelType::Placeholder => window::PLACEHOLDER, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, - ToplevelType::XWindow => window::X_WINDOW, + ToplevelType::XWindow { .. } => window::X_WINDOW, } } } diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index e809f8b3..8f1d27b9 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -4,6 +4,7 @@ use { crate::{ async_engine::SpawnedFuture, client::Client, + criteria::tlm::TL_CHANGED_CLASS_INST, ifs::{ ipc::{ DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable, @@ -1116,6 +1117,11 @@ impl Wm { async fn load_window_wm_class(&self, data: &Rc) { let mut buf = vec![]; + let property_changed = || { + if let Some(window) = data.window.get() { + window.toplevel_data.property_changed(TL_CHANGED_CLASS_INST); + } + }; match self .c .get_property::(data.window_id, ATOM_WM_CLASS, 0, &mut buf) @@ -1130,6 +1136,7 @@ impl Wm { Err(XconError::PropertyUnavailable) => { data.info.instance.borrow_mut().take(); data.info.class.borrow_mut().take(); + property_changed(); return; } Err(e) => { @@ -1138,8 +1145,10 @@ impl Wm { } } let mut iter = buf.split(|c| *c == 0); - *data.info.instance.borrow_mut() = Some(iter.next().unwrap_or(&[]).to_vec().into()); - *data.info.class.borrow_mut() = Some(iter.next().unwrap_or(&[]).to_vec().into()); + let mut map = || Some(iter.next().unwrap_or(&[]).to_str_lossy().into_owned()); + *data.info.instance.borrow_mut() = map(); + *data.info.class.borrow_mut() = map(); + property_changed(); } async fn load_window_wm_name2(&self, data: &Rc, prop: u32, name: &str) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index bd95a0e3..04076390 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -267,6 +267,10 @@ pub struct WindowMatch { pub just_mapped: Option, pub tag: Option, pub tag_regex: Option, + pub x_class: Option, + pub x_class_regex: Option, + pub x_instance: Option, + pub x_instance_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 f0f3eba8..e2d5ef2b 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -68,6 +68,7 @@ impl Parser for WindowMatchParser<'_> { tag, tag_regex, ), + (x_class, x_class_regex, x_instance, x_instance_regex), ) = ext.extract(( ( opt(str("name")), @@ -92,6 +93,12 @@ impl Parser for WindowMatchParser<'_> { opt(str("tag")), opt(str("tag-regex")), ), + ( + opt(str("x-class")), + opt(str("x-class-regex")), + opt(str("x-instance")), + opt(str("x-instance-regex")), + ), ))?; let mut not = None; if let Some(value) = not_val { @@ -144,6 +151,10 @@ impl Parser for WindowMatchParser<'_> { just_mapped: just_mapped.despan(), tag: tag.despan_into(), tag_regex: tag_regex.despan_into(), + x_class: x_class.despan_into(), + x_class_regex: x_class_regex.despan_into(), + x_instance: x_instance.despan_into(), + x_instance_regex: x_instance_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index c58f0f26..e03c03b9 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -260,6 +260,10 @@ impl Rule for WindowRule { value!(AppIdRegex, app_id_regex); value!(Tag, tag); value!(TagRegex, tag_regex); + value!(XClass, x_class); + value!(XClassRegex, x_class_regex); + value!(XInstance, x_instance); + value!(XInstanceRegex, x_instance_regex); bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 1bb7c8a8..72c6947d 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1827,6 +1827,22 @@ "tag-regex": { "type": "string", "description": "Matches the toplevel-tag of the window with a regular expression." + }, + "x-class": { + "type": "string", + "description": "Matches the X class of the window verbatim." + }, + "x-class-regex": { + "type": "string", + "description": "Matches the X class of the window with a regular expression." + }, + "x-instance": { + "type": "string", + "description": "Matches the X instance of the window verbatim." + }, + "x-instance-regex": { + "type": "string", + "description": "Matches the X instance 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 e5f0e8a4..90d80aca 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4085,6 +4085,30 @@ The table has the following fields: The value of this field should be a string. +- `x-class` (optional): + + Matches the X class of the window verbatim. + + The value of this field should be a string. + +- `x-class-regex` (optional): + + Matches the X class of the window with a regular expression. + + The value of this field should be a string. + +- `x-instance` (optional): + + Matches the X instance of the window verbatim. + + The value of this field should be a string. + +- `x-instance-regex` (optional): + + Matches the X instance 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 134dba12..e3f76683 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3519,6 +3519,22 @@ WindowMatch: kind: string required: false description: Matches the toplevel-tag of the window with a regular expression. + x-class: + kind: string + required: false + description: Matches the X class of the window verbatim. + x-class-regex: + kind: string + required: false + description: Matches the X class of the window with a regular expression. + x-instance: + kind: string + required: false + description: Matches the X instance of the window verbatim. + x-instance-regex: + kind: string + required: false + description: Matches the X instance of the window with a regular expression. WindowMatchExactly: From 5ad6ca4dd3b1da60ea26b106d73dab44336f8319 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 14:01:39 +0200 Subject: [PATCH 29/35] config: add WM_WINDOW_ROLE window criteria --- jay-config/src/_private.rs | 1 + jay-config/src/_private/client.rs | 2 ++ jay-config/src/window.rs | 4 ++++ src/config/handler.rs | 1 + src/criteria/tlm.rs | 11 ++++++++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 15 +++++++++++++++ src/ifs/wl_surface/x_surface/xwindow.rs | 2 +- src/xwayland/xwm.rs | 13 ++++++++++--- toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/window_match.rs | 6 +++++- 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 ++++++++ 14 files changed, 81 insertions(+), 6 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 6d666c2f..97d49c27 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -126,4 +126,5 @@ pub enum WindowCriterionStringField { Tag, XClass, XInstance, + XRole, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 14cd901b..963664a6 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1669,6 +1669,8 @@ impl ConfigClient { WindowCriterion::XClassRegex(t) => string!(t, XClass, true), WindowCriterion::XInstance(t) => string!(t, XInstance, false), WindowCriterion::XInstanceRegex(t) => string!(t, XInstance, true), + WindowCriterion::XRole(t) => string!(t, XRole, false), + WindowCriterion::XRoleRegex(t) => string!(t, XRole, 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 e2438e21..05ce9cbc 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -272,6 +272,10 @@ pub enum WindowCriterion<'a> { XInstance(&'a str), /// Matches the X instance of the window with a regular expression. XInstanceRegex(&'a str), + /// Matches the X role of the window verbatim. + XRole(&'a str), + /// Matches the X role of the window with a regular expression. + XRoleRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 6b9660f0..06ca2982 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1993,6 +1993,7 @@ impl ConfigProxyHandler { WindowCriterionStringField::Tag => mgr.tag(needle), WindowCriterionStringField::XClass => mgr.class(needle), WindowCriterionStringField::XInstance => mgr.instance(needle), + WindowCriterionStringField::XRole => mgr.role(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index e58a51ed..d22a7d48 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -19,7 +19,8 @@ use { tlmm_kind::TlmMatchKind, tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{ - TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchTag, TlmMatchTitle, + TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchRole, TlmMatchTag, + TlmMatchTitle, }, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, @@ -55,6 +56,7 @@ bitflags! { TL_CHANGED_JUST_MAPPED = 1 << 9, TL_CHANGED_TAG = 1 << 10, TL_CHANGED_CLASS_INST = 1 << 11, + TL_CHANGED_ROLE = 1 << 12, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -85,6 +87,7 @@ pub struct RootMatchers { seat_foci: TlmRootMatcherMap, class: TlmRootMatcherMap, instance: TlmRootMatcherMap, + role: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -215,6 +218,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TAG, tag); conditional!(TL_CHANGED_CLASS_INST, class); conditional!(TL_CHANGED_CLASS_INST, instance); + conditional!(TL_CHANGED_ROLE, role); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -290,6 +294,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_TAG, tag); conditional!(TL_CHANGED_CLASS_INST, class); conditional!(TL_CHANGED_CLASS_INST, instance); + conditional!(TL_CHANGED_ROLE, role); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -355,6 +360,10 @@ impl TlMatcherManager { pub fn instance(&self, string: CritLiteralOrRegex) -> Rc { self.root(TlmMatchInstance::new(string)) } + + pub fn role(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchRole::new(string)) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index 6800ecdf..dc75b3b4 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -13,12 +13,14 @@ pub type TlmMatchAppId = TlmMatchString; pub type TlmMatchTag = TlmMatchString; pub type TlmMatchClass = TlmMatchString; pub type TlmMatchInstance = TlmMatchString; +pub type TlmMatchRole = TlmMatchString; pub struct TitleAccess; pub struct AppIdAccess; pub struct TagAccess; pub struct ClassAccess; pub struct InstanceAccess; +pub struct RoleAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -78,3 +80,16 @@ impl StringAccess for InstanceAccess { &roots.instance } } + +impl StringAccess for RoleAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let ToplevelType::XWindow(data) = &data.kind { + return f(&data.info.role.borrow().as_deref().unwrap_or_default()); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.role + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index e0f497be..2bceaf68 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -93,7 +93,7 @@ pub struct XwindowInfo { pub instance: RefCell>, pub class: RefCell>, pub title: RefCell>, - pub role: RefCell>, + pub role: RefCell>, pub protocols: CopyHashMap, pub window_types: CopyHashMap, pub never_focus: Cell, diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index 8f1d27b9..7d22e3e3 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -4,7 +4,7 @@ use { crate::{ async_engine::SpawnedFuture, client::Client, - criteria::tlm::TL_CHANGED_CLASS_INST, + criteria::tlm::{TL_CHANGED_CLASS_INST, TL_CHANGED_ROLE}, ifs::{ ipc::{ DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable, @@ -60,7 +60,7 @@ use { xwayland::{XWaylandError, XWaylandEvent}, }, ahash::{AHashMap, AHashSet}, - bstr::ByteSlice, + bstr::{ByteSlice, ByteVec}, futures_util::{FutureExt, select}, smallvec::SmallVec, std::{ @@ -1086,6 +1086,11 @@ impl Wm { } async fn load_window_wm_window_role(&self, data: &Rc) { + let property_changed = || { + if let Some(window) = data.window.get() { + window.toplevel_data.property_changed(TL_CHANGED_ROLE); + } + }; let mut buf = vec![]; match self .c @@ -1101,6 +1106,7 @@ impl Wm { } Err(XconError::PropertyUnavailable) => { data.info.role.borrow_mut().take(); + property_changed(); return; } Err(e) => { @@ -1112,7 +1118,8 @@ impl Wm { } } // log::info!("{} role {}", data.window_id, buf.as_bstr()); - *data.info.role.borrow_mut() = Some(buf.into()); + *data.info.role.borrow_mut() = Some(buf.into_string_lossy()); + property_changed(); } async fn load_window_wm_class(&self, data: &Rc) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 04076390..9effaf3d 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -271,6 +271,8 @@ pub struct WindowMatch { pub x_class_regex: Option, pub x_instance: Option, pub x_instance_regex: Option, + pub x_role: Option, + pub x_role_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 e2d5ef2b..ca4291bf 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -68,7 +68,7 @@ impl Parser for WindowMatchParser<'_> { tag, tag_regex, ), - (x_class, x_class_regex, x_instance, x_instance_regex), + (x_class, x_class_regex, x_instance, x_instance_regex, x_role, x_role_regex), ) = ext.extract(( ( opt(str("name")), @@ -98,6 +98,8 @@ impl Parser for WindowMatchParser<'_> { opt(str("x-class-regex")), opt(str("x-instance")), opt(str("x-instance-regex")), + opt(str("x-role")), + opt(str("x-role-regex")), ), ))?; let mut not = None; @@ -155,6 +157,8 @@ impl Parser for WindowMatchParser<'_> { x_class_regex: x_class_regex.despan_into(), x_instance: x_instance.despan_into(), x_instance_regex: x_instance_regex.despan_into(), + x_role: x_role.despan_into(), + x_role_regex: x_role_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index e03c03b9..b3b26228 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -264,6 +264,8 @@ impl Rule for WindowRule { value!(XClassRegex, x_class_regex); value!(XInstance, x_instance); value!(XInstanceRegex, x_instance_regex); + value!(XRole, x_role); + value!(XRoleRegex, x_role_regex); bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 72c6947d..6038425b 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1843,6 +1843,14 @@ "x-instance-regex": { "type": "string", "description": "Matches the X instance of the window with a regular expression." + }, + "x-role": { + "type": "string", + "description": "Matches the X role of the window verbatim." + }, + "x-role-regex": { + "type": "string", + "description": "Matches the X role 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 90d80aca..1bdd199b 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4109,6 +4109,18 @@ The table has the following fields: The value of this field should be a string. +- `x-role` (optional): + + Matches the X role of the window verbatim. + + The value of this field should be a string. + +- `x-role-regex` (optional): + + Matches the X role 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 e3f76683..9f9dc04b 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3535,6 +3535,14 @@ WindowMatch: kind: string required: false description: Matches the X instance of the window with a regular expression. + x-role: + kind: string + required: false + description: Matches the X role of the window verbatim. + x-role-regex: + kind: string + required: false + description: Matches the X role of the window with a regular expression. WindowMatchExactly: From 51e752992fa10b3b608685f124374c14d8306917 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 14:13:40 +0200 Subject: [PATCH 30/35] config: add workspace window criteria --- jay-config/src/_private.rs | 3 +++ jay-config/src/_private/client.rs | 3 +++ jay-config/src/window.rs | 6 ++++++ src/config/handler.rs | 4 ++++ src/criteria/tlm.rs | 10 +++++++++- src/criteria/tlm/tlm_matchers/tlmm_string.rs | 15 +++++++++++++++ src/tree/toplevel.rs | 3 ++- toml-config/src/config.rs | 2 ++ toml-config/src/config/parsers/window_match.rs | 15 ++++++++++++++- 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, 88 insertions(+), 3 deletions(-) diff --git a/jay-config/src/_private.rs b/jay-config/src/_private.rs index 97d49c27..d968223e 100644 --- a/jay-config/src/_private.rs +++ b/jay-config/src/_private.rs @@ -5,6 +5,7 @@ pub(crate) mod string_error; use { crate::{ + Workspace, client::ClientMatcher, input::Seat, video::Mode, @@ -117,6 +118,7 @@ pub enum WindowCriterionIpc { SeatFocus(Seat), Fullscreen, JustMapped, + Workspace(Workspace), } #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] @@ -127,4 +129,5 @@ pub enum WindowCriterionStringField { XClass, XInstance, XRole, + Workspace, } diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 963664a6..63eda9cf 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1671,6 +1671,9 @@ impl ConfigClient { WindowCriterion::XInstanceRegex(t) => string!(t, XInstance, true), WindowCriterion::XRole(t) => string!(t, XRole, false), WindowCriterion::XRoleRegex(t) => string!(t, XRole, true), + WindowCriterion::Workspace(t) => WindowCriterionIpc::Workspace(t), + WindowCriterion::WorkspaceName(t) => string!(t, Workspace, false), + WindowCriterion::WorkspaceNameRegex(t) => string!(t, Workspace, 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 05ce9cbc..f7996d6f 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -276,6 +276,12 @@ pub enum WindowCriterion<'a> { XRole(&'a str), /// Matches the X role of the window with a regular expression. XRoleRegex(&'a str), + /// Matches the workspace the window. + Workspace(Workspace), + /// Matches the workspace name of the window verbatim. + WorkspaceName(&'a str), + /// Matches the workspace name of the window with a regular expression. + WorkspaceNameRegex(&'a str), } impl WindowCriterion<'_> { diff --git a/src/config/handler.rs b/src/config/handler.rs index 06ca2982..0b10eb2f 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1994,6 +1994,7 @@ impl ConfigProxyHandler { WindowCriterionStringField::XClass => mgr.class(needle), WindowCriterionStringField::XInstance => mgr.instance(needle), WindowCriterionStringField::XRole => mgr.role(needle), + WindowCriterionStringField::Workspace => mgr.workspace(needle), } } WindowCriterionIpc::Types(t) => mgr.kind(*t), @@ -2007,6 +2008,9 @@ impl ConfigProxyHandler { WindowCriterionIpc::SeatFocus(seat) => mgr.seat_focus(&*self.get_seat(*seat)?), WindowCriterionIpc::Fullscreen => mgr.fullscreen(), WindowCriterionIpc::JustMapped => mgr.just_mapped(), + WindowCriterionIpc::Workspace(w) => mgr.workspace(CritLiteralOrRegex::Literal( + self.get_workspace(*w)?.to_string(), + )), }; let cached = Rc::new(CachedCriterion { crit: criterion.clone(), diff --git a/src/criteria/tlm.rs b/src/criteria/tlm.rs index d22a7d48..cdb01a6f 100644 --- a/src/criteria/tlm.rs +++ b/src/criteria/tlm.rs @@ -20,7 +20,7 @@ use { tlmm_seat_focus::TlmMatchSeatFocus, tlmm_string::{ TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchRole, TlmMatchTag, - TlmMatchTitle, + TlmMatchTitle, TlmMatchWorkspace, }, tlmm_urgent::TlmMatchUrgent, tlmm_visible::TlmMatchVisible, @@ -57,6 +57,7 @@ bitflags! { TL_CHANGED_TAG = 1 << 10, TL_CHANGED_CLASS_INST = 1 << 11, TL_CHANGED_ROLE = 1 << 12, + TL_CHANGED_WORKSPACE = 1 << 13, } type TlmFixedRootMatcher = FixedRootMatcher; @@ -88,6 +89,7 @@ pub struct RootMatchers { class: TlmRootMatcherMap, instance: TlmRootMatcherMap, role: TlmRootMatcherMap, + workspace: TlmRootMatcherMap, } pub async fn handle_tl_changes(state: Rc) { @@ -219,6 +221,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_CLASS_INST, class); conditional!(TL_CHANGED_CLASS_INST, instance); conditional!(TL_CHANGED_ROLE, role); + conditional!(TL_CHANGED_WORKSPACE, workspace); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -295,6 +298,7 @@ impl TlMatcherManager { conditional!(TL_CHANGED_CLASS_INST, class); conditional!(TL_CHANGED_CLASS_INST, instance); conditional!(TL_CHANGED_ROLE, role); + conditional!(TL_CHANGED_WORKSPACE, workspace); fixed_conditional!(TL_CHANGED_FLOATING, floating); fixed_conditional!(TL_CHANGED_VISIBLE, visible); fixed_conditional!(TL_CHANGED_URGENT, urgent); @@ -364,6 +368,10 @@ impl TlMatcherManager { pub fn role(&self, string: CritLiteralOrRegex) -> Rc { self.root(TlmMatchRole::new(string)) } + + pub fn workspace(&self, string: CritLiteralOrRegex) -> Rc { + self.root(TlmMatchWorkspace::new(string)) + } } impl CritTarget for ToplevelData { diff --git a/src/criteria/tlm/tlm_matchers/tlmm_string.rs b/src/criteria/tlm/tlm_matchers/tlmm_string.rs index dc75b3b4..62413481 100644 --- a/src/criteria/tlm/tlm_matchers/tlmm_string.rs +++ b/src/criteria/tlm/tlm_matchers/tlmm_string.rs @@ -14,6 +14,7 @@ pub type TlmMatchTag = TlmMatchString; pub type TlmMatchClass = TlmMatchString; pub type TlmMatchInstance = TlmMatchString; pub type TlmMatchRole = TlmMatchString; +pub type TlmMatchWorkspace = TlmMatchString; pub struct TitleAccess; pub struct AppIdAccess; @@ -21,6 +22,7 @@ pub struct TagAccess; pub struct ClassAccess; pub struct InstanceAccess; pub struct RoleAccess; +pub struct WorkspaceAccess; impl StringAccess for TitleAccess { fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { @@ -93,3 +95,16 @@ impl StringAccess for RoleAccess { &roots.role } } + +impl StringAccess for WorkspaceAccess { + fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool { + if let Some(ws) = data.workspace.get() { + return f(&ws.name); + } + false + } + + fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap> { + &roots.workspace + } +} diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 6992aac6..adcef45e 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -6,7 +6,7 @@ use { 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, TlMatcherChange, + TL_CHANGED_VISIBLE, TL_CHANGED_WORKSPACE, TlMatcherChange, }, }, ifs::{ @@ -131,6 +131,7 @@ impl ToplevelNode for T { let data = self.tl_data(); let prev = data.workspace.set(Some(ws.clone())); self.tl_set_workspace_ext(ws); + self.tl_data().property_changed(TL_CHANGED_WORKSPACE); let prev_id = prev.map(|p| p.output.get().id); let new_id = Some(ws.output.get().id); if prev_id != new_id { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 9effaf3d..553ec51b 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -273,6 +273,8 @@ pub struct WindowMatch { pub x_instance_regex: Option, pub x_role: Option, pub x_role_regex: Option, + pub workspace: Option, + pub workspace_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 ca4291bf..b8479762 100644 --- a/toml-config/src/config/parsers/window_match.rs +++ b/toml-config/src/config/parsers/window_match.rs @@ -68,7 +68,16 @@ impl Parser for WindowMatchParser<'_> { tag, tag_regex, ), - (x_class, x_class_regex, x_instance, x_instance_regex, x_role, x_role_regex), + ( + x_class, + x_class_regex, + x_instance, + x_instance_regex, + x_role, + x_role_regex, + workspace, + workspace_regex, + ), ) = ext.extract(( ( opt(str("name")), @@ -100,6 +109,8 @@ impl Parser for WindowMatchParser<'_> { opt(str("x-instance-regex")), opt(str("x-role")), opt(str("x-role-regex")), + opt(str("workspace")), + opt(str("workspace-regex")), ), ))?; let mut not = None; @@ -159,6 +170,8 @@ impl Parser for WindowMatchParser<'_> { x_instance_regex: x_instance_regex.despan_into(), x_role: x_role.despan_into(), x_role_regex: x_role_regex.despan_into(), + workspace: workspace.despan_into(), + workspace_regex: workspace_regex.despan_into(), types, client, }) diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index b3b26228..f19d31fc 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -266,6 +266,8 @@ impl Rule for WindowRule { value!(XInstanceRegex, x_instance_regex); value!(XRole, x_role); value!(XRoleRegex, x_role_regex); + value!(WorkspaceName, workspace); + value!(WorkspaceNameRegex, workspace_regex); bool!(Floating, floating); bool!(Visible, visible); bool!(Urgent, urgent); diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 6038425b..d248c695 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1851,6 +1851,14 @@ "x-role-regex": { "type": "string", "description": "Matches the X role of the window with a regular expression." + }, + "workspace": { + "type": "string", + "description": "Matches the workspace of the window verbatim." + }, + "workspace-regex": { + "type": "string", + "description": "Matches the workspace 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 1bdd199b..2e249c4d 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4121,6 +4121,18 @@ The table has the following fields: The value of this field should be a string. +- `workspace` (optional): + + Matches the workspace of the window verbatim. + + The value of this field should be a string. + +- `workspace-regex` (optional): + + Matches the workspace 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 9f9dc04b..201a3c17 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3543,6 +3543,14 @@ WindowMatch: kind: string required: false description: Matches the X role of the window with a regular expression. + workspace: + kind: string + required: false + description: Matches the workspace of the window verbatim. + workspace-regex: + kind: string + required: false + description: Matches the workspace of the window with a regular expression. WindowMatchExactly: From b1ca98b48805e9a6e5aba34533168fcb133d7d00 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 3 May 2025 15:33:02 +0200 Subject: [PATCH 31/35] config: add auto-focus window rule --- jay-config/src/_private/client.rs | 7 ++++ jay-config/src/_private/ipc.rs | 4 +++ jay-config/src/window.rs | 18 +++++++++++ src/config.rs | 9 ++++++ src/config/handler.rs | 32 +++++++++++++++++++ src/state.rs | 23 ++++++++----- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/window_rule.rs | 6 ++-- toml-config/src/rules.rs | 3 ++ toml-spec/spec/spec.generated.json | 4 +++ toml-spec/spec/spec.generated.md | 9 ++++++ toml-spec/spec/spec.yaml | 8 +++++ 12 files changed, 114 insertions(+), 10 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 63eda9cf..2991b43d 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1701,6 +1701,13 @@ impl ConfigClient { handler.cb = cb.clone(); } + pub fn set_window_matcher_auto_focus(&self, matcher: WindowMatcher, auto_focus: bool) { + self.send(&ClientMessage::SetWindowMatcherAutoFocus { + matcher, + auto_focus, + }); + } + pub fn set_window_matcher_latch_handler( &self, matcher: WindowMatcher, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 384c13ae..50d01c68 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -698,6 +698,10 @@ pub enum ClientMessage<'a> { EnableWindowMatcherEvents { matcher: WindowMatcher, }, + SetWindowMatcherAutoFocus { + matcher: WindowMatcher, + auto_focus: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index f7996d6f..222e1ac2 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -296,6 +296,16 @@ impl WindowCriterion<'_> { pub fn bind(self, cb: F) { self.to_matcher().bind(cb); } + + /// Sets whether newly mapped windows that match this criterion get the keyboard focus. + /// + /// If a window matches any criterion for which this is false, the window will not be + /// automatically focused. + /// + /// This leaks the matcher. + pub fn set_auto_focus(self, auto_focus: bool) { + self.to_matcher().set_auto_focus(auto_focus); + } } impl WindowMatcher { @@ -312,6 +322,14 @@ impl WindowMatcher { pub fn bind(self, cb: F) { get!().set_window_matcher_handler(self, cb); } + + /// Sets whether newly mapped windows that match this matcher get the keyboard focus. + /// + /// If a window matches any matcher for which this is false, the window will not be + /// automatically focused. + pub fn set_auto_focus(self, auto_focus: bool) { + get!().set_window_matcher_auto_focus(self, auto_focus); + } } impl MatchedWindow { diff --git a/src/config.rs b/src/config.rs index 841e3fad..7655568b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,7 @@ use { config::handler::ConfigProxyHandler, ifs::wl_seat::SeatId, state::State, + tree::ToplevelData, utils::{ clonecell::CloneCell, numcell::NumCell, ptr_ext::PtrExt, toplevel_identifier::ToplevelIdentifier, unlink_on_drop::UnlinkOnDrop, xrd::xrd, @@ -161,6 +162,13 @@ impl ConfigProxy { handler.windows_to_tl_id.remove(&win); } } + + pub fn auto_focus(&self, data: &ToplevelData) -> bool { + let Some(handler) = self.handler.get() else { + return true; + }; + handler.auto_focus(data) + } } impl Drop for ConfigProxy { @@ -224,6 +232,7 @@ impl ConfigProxy { window_matcher_cache: Default::default(), window_matcher_leafs: Default::default(), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), + window_matcher_no_auto_focus: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index 0b10eb2f..aa4996a1 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -124,6 +124,8 @@ pub(super) struct ConfigProxyHandler { pub window_matcher_cache: CriterionCache, pub window_matcher_leafs: CopyHashMap>, pub window_matcher_std_kinds: Rc, + pub window_matcher_no_auto_focus: + CopyHashMap>>, } pub struct Pollable { @@ -2027,6 +2029,7 @@ impl ConfigProxyHandler { fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) { self.window_matchers.remove(&matcher); self.window_matcher_leafs.remove(&matcher); + self.window_matcher_no_auto_focus.remove(&matcher); } fn handle_enable_window_matcher_events( @@ -2056,6 +2059,20 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_matcher_auto_focus( + &self, + matcher: WindowMatcher, + auto_focus: bool, + ) -> Result<(), CphError> { + if auto_focus { + self.window_matcher_no_auto_focus.remove(&matcher); + } else { + let m = self.get_window_matcher(matcher)?; + self.window_matcher_no_auto_focus.set(matcher, m); + } + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2861,9 +2878,24 @@ impl ConfigProxyHandler { ClientMessage::EnableWindowMatcherEvents { matcher } => self .handle_enable_window_matcher_events(matcher) .wrn("enable_window_matcher_events")?, + ClientMessage::SetWindowMatcherAutoFocus { + matcher, + auto_focus, + } => self + .handle_set_window_matcher_auto_focus(matcher, auto_focus) + .wrn("set_window_matcher_auto_focus")?, } Ok(()) } + + pub fn auto_focus(&self, data: &ToplevelData) -> bool { + for matcher in self.window_matcher_no_auto_focus.lock().values() { + if matcher.node.pull(data) { + return false; + } + } + true + } } #[derive(Debug, Error)] diff --git a/src/state.rs b/src/state.rs index f870ac9e..e93e67fe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -665,11 +665,7 @@ impl State { pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); self.do_map_tiled(seat.as_deref(), node.clone()); - if node.node_visible() { - if let Some(seat) = seat { - node.node_do_focus(&seat, Direction::Unspecified); - } - } + self.focus_after_map(node, seat.as_deref()); } fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { @@ -739,11 +735,22 @@ impl State { Rect::new_sized(x1, y1, width, height).unwrap() }; FloatNode::new(self, workspace, position, node.clone()); - if node.node_visible() { - if let Some(seat) = self.seat_queue.last() { - node.node_do_focus(&seat, Direction::Unspecified); + self.focus_after_map(node, self.seat_queue.last().as_deref()); + } + + fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { + if !node.node_visible() { + return; + } + let Some(seat) = seat else { + return; + }; + if let Some(config) = self.config.get() { + if !config.auto_focus(node.tl_data()) { + return; } } + node.node_do_focus(&seat, Direction::Unspecified); } pub fn show_workspace(&self, seat: &Rc, name: &str) { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 553ec51b..52f18fde 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -248,6 +248,7 @@ pub struct WindowRule { pub match_: WindowMatch, pub action: Option, pub latch: Option, + pub auto_focus: Option, } #[derive(Default, Debug, Clone)] diff --git a/toml-config/src/config/parsers/window_rule.rs b/toml-config/src/config/parsers/window_rule.rs index a31ab978..21405e60 100644 --- a/toml-config/src/config/parsers/window_rule.rs +++ b/toml-config/src/config/parsers/window_rule.rs @@ -3,7 +3,7 @@ use { config::{ WindowMatch, WindowRule, context::Context, - extractor::{Extractor, ExtractorError, opt, str, val}, + extractor::{Extractor, ExtractorError, bol, opt, recover, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::{ActionParser, ActionParserError}, @@ -47,11 +47,12 @@ impl Parser for WindowRuleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (name, match_val, action_val, latch_val) = ext.extract(( + let (name, match_val, action_val, latch_val, auto_focus) = ext.extract(( opt(str("name")), opt(val("match")), opt(val("action")), opt(val("latch")), + recover(opt(bol("auto-focus"))), ))?; let mut action = None; if let Some(value) = action_val { @@ -78,6 +79,7 @@ impl Parser for WindowRuleParser<'_> { match_, action, latch, + auto_focus: auto_focus.despan(), }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index f19d31fc..93ee47e2 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -333,6 +333,9 @@ impl Rule for WindowRule { }); } } + if let Some(auto_focus) = self.auto_focus { + matcher.set_auto_focus(auto_focus); + } } fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index d248c695..7a91ce7b 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1904,6 +1904,10 @@ "latch": { "description": "An action to execute when a window no longer matches the criteria.", "$ref": "#/$defs/Action" + }, + "auto-focus": { + "type": "boolean", + "description": "Whether newly mapped windows that match this rule get the keyboard focus.\n\nIf a window matches any rule for which this is false, the window will not be\nautomatically focused.\n" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 2e249c4d..c86bc249 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4203,6 +4203,15 @@ The table has the following fields: The value of this field should be a [Action](#types-Action). +- `auto-focus` (optional): + + Whether newly mapped windows that match this rule get the keyboard focus. + + If a window matches any rule for which this is false, the window will not be + automatically focused. + + The value of this field should be a boolean. + ### `WindowTypeMask` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 201a3c17..73914d9a 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3368,6 +3368,14 @@ WindowRule: ref: Action required: false description: An action to execute when a window no longer matches the criteria. + auto-focus: + kind: boolean + required: false + description: | + Whether newly mapped windows that match this rule get the keyboard focus. + + If a window matches any rule for which this is false, the window will not be + automatically focused. WindowMatch: From 5e3465d8612f1c83c7f96a1d614a22f7ca239209 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 7 May 2025 15:59:42 +0200 Subject: [PATCH 32/35] config: add initial-tile-state window rule --- jay-config/src/_private/client.rs | 13 ++++++- jay-config/src/_private/ipc.rs | 6 +++- jay-config/src/window.rs | 30 ++++++++++++++++ src/config.rs | 7 +++- src/config/handler.rs | 36 ++++++++++++++++++- src/ifs/wl_surface/x_surface/xwindow.rs | 11 +++++- .../wl_surface/xdg_surface/xdg_toplevel.rs | 32 ++++++++++++++--- src/state.rs | 26 +++++++++----- toml-config/src/config.rs | 3 +- toml-config/src/config/parsers.rs | 1 + toml-config/src/config/parsers/tile_state.rs | 35 ++++++++++++++++++ toml-config/src/config/parsers/window_rule.rs | 30 ++++++++++++---- toml-config/src/rules.rs | 3 ++ toml-spec/spec/spec.generated.json | 12 +++++++ toml-spec/spec/spec.generated.md | 25 +++++++++++++ toml-spec/spec/spec.yaml | 14 ++++++++ 16 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 toml-config/src/config/parsers/tile_state.rs diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 2991b43d..e03ec7fb 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -32,7 +32,7 @@ use { Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, - window::{MatchedWindow, Window, WindowCriterion, WindowMatcher, WindowType}, + window::{MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType}, xwayland::XScalingMode, }, bincode::Options, @@ -1708,6 +1708,17 @@ impl ConfigClient { }); } + pub fn set_window_matcher_initial_tile_state( + &self, + matcher: WindowMatcher, + tile_state: TileState, + ) { + self.send(&ClientMessage::SetWindowMatcherInitialTileState { + matcher, + tile_state, + }); + } + pub fn set_window_matcher_latch_handler( &self, matcher: WindowMatcher, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 50d01c68..6aa10326 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::{Window, WindowMatcher, WindowType}, + window::{TileState, Window, WindowMatcher, WindowType}, xwayland::XScalingMode, }, serde::{Deserialize, Serialize}, @@ -702,6 +702,10 @@ pub enum ClientMessage<'a> { matcher: WindowMatcher, auto_focus: bool, }, + SetWindowMatcherInitialTileState { + matcher: WindowMatcher, + tile_state: TileState, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/window.rs b/jay-config/src/window.rs index 222e1ac2..df949770 100644 --- a/jay-config/src/window.rs +++ b/jay-config/src/window.rs @@ -41,6 +41,16 @@ bitflags! { } } +/// The tile state of a window. +#[non_exhaustive] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub enum TileState { + /// The window is tiled. + Tiled, + /// The window is floating. + Floating, +} + /// A window created by a client. /// /// This is the same as `XDG_TOPLEVEL | X_WINDOW`. @@ -306,6 +316,17 @@ impl WindowCriterion<'_> { pub fn set_auto_focus(self, auto_focus: bool) { self.to_matcher().set_auto_focus(auto_focus); } + + /// Sets whether newly mapped windows that match this matcher are mapped tiling or + /// floating. + /// + /// If multiple such window matchers match a window, the used tile state is + /// unspecified. + /// + /// This leaks the matcher. + pub fn set_initial_tile_state(self, tile_state: TileState) { + self.to_matcher().set_initial_tile_state(tile_state); + } } impl WindowMatcher { @@ -330,6 +351,15 @@ impl WindowMatcher { pub fn set_auto_focus(self, auto_focus: bool) { get!().set_window_matcher_auto_focus(self, auto_focus); } + + /// Sets whether newly mapped windows that match this matcher are mapped tiling or + /// floating. + /// + /// If multiple such window matchers match a window, the used tile state is + /// unspecified. + pub fn set_initial_tile_state(self, tile_state: TileState) { + get!().set_window_matcher_initial_tile_state(self, tile_state); + } } impl MatchedWindow { diff --git a/src/config.rs b/src/config.rs index 7655568b..ebc993cd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,7 @@ use { input::{InputDevice, Seat, SwitchEvent}, keyboard::{mods::Modifiers, syms::KeySym}, video::{Connector, DrmDevice}, - window, + window::{self, TileState}, }, libloading::Library, std::{cell::Cell, io, mem, ptr, rc::Rc}, @@ -169,6 +169,10 @@ impl ConfigProxy { }; handler.auto_focus(data) } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + self.handler.get()?.initial_tile_state(data) + } } impl Drop for ConfigProxy { @@ -233,6 +237,7 @@ impl ConfigProxy { window_matcher_leafs: Default::default(), window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW), window_matcher_no_auto_focus: Default::default(), + window_matcher_initial_tile_state: Default::default(), }); let init_msg = bincode_ops() .serialize(&InitMessage::V1(V1InitMessage {})) diff --git a/src/config/handler.rs b/src/config/handler.rs index aa4996a1..9f693fe6 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -66,7 +66,7 @@ use { TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, - window::{Window, WindowMatcher}, + window::{TileState, Window, WindowMatcher}, xwayland::XScalingMode, }, libloading::Library, @@ -126,6 +126,13 @@ pub(super) struct ConfigProxyHandler { pub window_matcher_std_kinds: Rc, pub window_matcher_no_auto_focus: CopyHashMap>>, + pub window_matcher_initial_tile_state: CopyHashMap< + WindowMatcher, + ( + Rc>, + TileState, + ), + >, } pub struct Pollable { @@ -2030,6 +2037,7 @@ impl ConfigProxyHandler { self.window_matchers.remove(&matcher); self.window_matcher_leafs.remove(&matcher); self.window_matcher_no_auto_focus.remove(&matcher); + self.window_matcher_initial_tile_state.remove(&matcher); } fn handle_enable_window_matcher_events( @@ -2073,6 +2081,17 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_matcher_initial_tile_state( + &self, + matcher: WindowMatcher, + tile_state: TileState, + ) -> Result<(), CphError> { + let m = self.get_window_matcher(matcher)?; + self.window_matcher_initial_tile_state + .set(matcher, (m, tile_state)); + Ok(()) + } + fn spaces_change(&self) { struct V; impl NodeVisitorBase for V { @@ -2884,6 +2903,12 @@ impl ConfigProxyHandler { } => self .handle_set_window_matcher_auto_focus(matcher, auto_focus) .wrn("set_window_matcher_auto_focus")?, + ClientMessage::SetWindowMatcherInitialTileState { + matcher, + tile_state, + } => self + .handle_set_window_matcher_initial_tile_state(matcher, tile_state) + .wrn("set_window_matcher_initial_tile_state")?, } Ok(()) } @@ -2896,6 +2921,15 @@ impl ConfigProxyHandler { } true } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + for (matcher, state) in self.window_matcher_initial_tile_state.lock().values() { + if matcher.node.pull(data) { + return Some(*state); + } + } + None + } } #[derive(Debug, Error)] diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index 2bceaf68..46b950fb 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -21,6 +21,7 @@ use { xwayland::XWaylandEvent, }, bstr::BString, + jay_config::window::TileState, std::{ cell::{Cell, RefCell}, ops::{Deref, Not}, @@ -266,6 +267,14 @@ impl Xwindow { pub fn map_status_changed(self: &Rc) { let map_change = self.map_change(); let override_redirect = self.data.info.override_redirect.get(); + let map_floating = match self + .toplevel_data + .state + .initial_tile_state(&self.toplevel_data) + { + None => self.data.info.wants_floating.get(), + Some(m) => m == TileState::Floating, + }; match map_change { Change::None => return, Change::Unmap => { @@ -282,7 +291,7 @@ impl Xwindow { Some(self.data.state.root.stacked.add_last(self.clone())); self.data.state.tree_changed(); } - Change::Map if self.data.info.wants_floating.get() => { + Change::Map if map_floating => { let ws = self.data.state.float_map_ws(); let ext = self.data.info.pending_extents.get(); self.data diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 9bdc3d3c..9a824027 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -34,6 +34,7 @@ use { wire::{XdgToplevelId, xdg_toplevel::*}, }, ahash::{AHashMap, AHashSet}, + jay_config::window::TileState, num_derive::FromPrimitive, std::{ cell::{Cell, RefCell}, @@ -381,6 +382,31 @@ impl XdgToplevelRequestHandler for XdgToplevel { } impl XdgToplevel { + fn map( + self: &Rc, + parent: Option<&XdgToplevel>, + pos: Option<(&Rc, i32, i32)>, + ) { + if let Some(state) = self.state.initial_tile_state(&self.toplevel_data) { + match state { + TileState::Floating => { + let mut ws = None; + if let Some(parent) = parent { + ws = parent.xdg.workspace.get(); + } + let ws = ws.unwrap_or_else(|| self.state.ensure_map_workspace(None)); + self.map_floating(&ws, pos.map(|p| (p.1, p.2))); + } + _ => self.map_tiled(), + } + return; + } + match parent { + None => self.map_tiled(), + Some(p) => self.map_child(p, pos), + } + } + fn map_floating(self: &Rc, workspace: &Rc, abs_pos: Option<(i32, i32)>) { let (width, height) = self.toplevel_data.float_size(workspace); self.state @@ -474,11 +500,7 @@ impl XdgToplevel { } self.state.tree_changed(); } else { - if let Some(parent) = self.parent.get() { - self.map_child(&parent, pos); - } else { - self.map_tiled(); - } + self.map(self.parent.get().as_deref(), pos); self.extents_changed(); if let Some(workspace) = self.xdg.workspace.get() { let output = workspace.output.get(); diff --git a/src/state.rs b/src/state.rs index e93e67fe..b03cdddc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -82,8 +82,8 @@ use { time::Time, tree::{ ContainerNode, ContainerSplit, Direction, DisplayNode, FloatNode, LatchListener, Node, - NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelNode, - ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, + NodeIds, NodeVisitorBase, OutputNode, PlaceholderNode, TearingMode, ToplevelData, + ToplevelNode, ToplevelNodeBase, VrrMode, WorkspaceNode, generic_node_visitor, }, utils::{ activation_token::ActivationToken, asyncevent::AsyncEvent, bindings::Bindings, @@ -112,6 +112,7 @@ use { jay_config::{ PciId, video::{GfxApi, Transform}, + window::TileState, }, std::{ cell::{Cell, RefCell}, @@ -662,6 +663,16 @@ impl State { } } + pub fn ensure_map_workspace(&self, seat: Option<&Rc>) -> Rc { + seat.cloned() + .or_else(|| self.seat_queue.last().map(|s| s.deref().clone())) + .map(|s| s.get_output()) + .or_else(|| self.root.outputs.lock().values().next().cloned()) + .or_else(|| self.dummy_output.get()) + .unwrap() + .ensure_workspace() + } + pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); self.do_map_tiled(seat.as_deref(), node.clone()); @@ -669,12 +680,7 @@ impl State { } fn do_map_tiled(self: &Rc, seat: Option<&Rc>, node: Rc) { - let output = seat - .map(|s| s.get_output()) - .or_else(|| self.root.outputs.lock().values().next().cloned()) - .or_else(|| self.dummy_output.get()) - .unwrap(); - let ws = output.ensure_workspace(); + let ws = self.ensure_map_workspace(seat); self.map_tiled_on(node, &ws); } @@ -1384,6 +1390,10 @@ impl State { }; ctx.supports_color_management() } + + pub fn initial_tile_state(&self, data: &ToplevelData) -> Option { + self.config.get()?.initial_tile_state(data) + } } #[derive(Debug, Error)] diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 52f18fde..06bf1873 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::WindowType, + window::{TileState, WindowType}, xwayland::XScalingMode, }, std::{ @@ -249,6 +249,7 @@ pub struct WindowRule { pub action: Option, pub latch: Option, pub auto_focus: Option, + pub initial_tile_state: Option, } #[derive(Default, Debug, Clone)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index d49fabfc..ed064b44 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -37,6 +37,7 @@ pub mod shortcuts; mod status; mod tearing; mod theme; +mod tile_state; mod ui_drag; mod vrr; mod window_match; diff --git a/toml-config/src/config/parsers/tile_state.rs b/toml-config/src/config/parsers/tile_state.rs new file mode 100644 index 00000000..de9d30eb --- /dev/null +++ b/toml-config/src/config/parsers/tile_state.rs @@ -0,0 +1,35 @@ +use { + crate::{ + config::parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + toml::toml_span::{Span, SpannedExt}, + }, + jay_config::window::TileState, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum TileStateParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Unknown tile state `{}`", .0)] + UnknownTileState(String), +} + +pub struct TileStateParser; + +impl Parser for TileStateParser { + type Value = TileState; + type Error = TileStateParserError; + const EXPECTED: &'static [DataType] = &[DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + let ty = match string { + "tiled" => TileState::Tiled, + "floating" => TileState::Floating, + _ => { + return Err(TileStateParserError::UnknownTileState(string.to_owned()).spanned(span)); + } + }; + Ok(ty) + } +} diff --git a/toml-config/src/config/parsers/window_rule.rs b/toml-config/src/config/parsers/window_rule.rs index 21405e60..5311641c 100644 --- a/toml-config/src/config/parsers/window_rule.rs +++ b/toml-config/src/config/parsers/window_rule.rs @@ -7,6 +7,7 @@ use { parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::{ActionParser, ActionParserError}, + tile_state::TileStateParser, window_match::{WindowMatchParser, WindowMatchParserError}, }, spanned::SpannedErrorExt, @@ -47,13 +48,15 @@ impl Parser for WindowRuleParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (name, match_val, action_val, latch_val, auto_focus) = ext.extract(( - opt(str("name")), - opt(val("match")), - opt(val("action")), - opt(val("latch")), - recover(opt(bol("auto-focus"))), - ))?; + let (name, match_val, action_val, latch_val, auto_focus, initial_tile_state_val) = ext + .extract(( + opt(str("name")), + opt(val("match")), + opt(val("action")), + opt(val("latch")), + recover(opt(bol("auto-focus"))), + opt(val("initial-tile-state")), + ))?; let mut action = None; if let Some(value) = action_val { action = Some( @@ -70,6 +73,18 @@ impl Parser for WindowRuleParser<'_> { .map_spanned_err(WindowRuleParserError::Latch)?, ); } + let mut initial_tile_state = None; + if let Some(value) = initial_tile_state_val { + match value.parse(&mut TileStateParser) { + Ok(v) => initial_tile_state = Some(v), + Err(e) => { + log::warn!( + "Could not parse the initial tile state: {}", + self.0.error(e) + ); + } + } + } let match_ = match match_val { None => WindowMatch::default(), Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?, @@ -80,6 +95,7 @@ impl Parser for WindowRuleParser<'_> { action, latch, auto_focus: auto_focus.despan(), + initial_tile_state, }) } } diff --git a/toml-config/src/rules.rs b/toml-config/src/rules.rs index 93ee47e2..ffef34c3 100644 --- a/toml-config/src/rules.rs +++ b/toml-config/src/rules.rs @@ -336,6 +336,9 @@ impl Rule for WindowRule { if let Some(auto_focus) = self.auto_focus { matcher.set_auto_focus(auto_focus); } + if let Some(tile_state) = self.initial_tile_state { + matcher.set_initial_tile_state(tile_state); + } } fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 7a91ce7b..d2c1b491 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1664,6 +1664,14 @@ }, "required": [] }, + "TileState": { + "type": "string", + "description": "Whether a window is tiled or floating.", + "enum": [ + "tiled", + "floating" + ] + }, "TransferFunction": { "type": "string", "description": "The transfer function of an output.\n", @@ -1908,6 +1916,10 @@ "auto-focus": { "type": "boolean", "description": "Whether newly mapped windows that match this rule get the keyboard focus.\n\nIf a window matches any rule for which this is false, the window will not be\nautomatically focused.\n" + }, + "initial-tile-state": { + "description": "Specifies if the window is initially mapped tiled or floating.", + "$ref": "#/$defs/TileState" } }, "required": [] diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index c86bc249..96e76355 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3710,6 +3710,25 @@ The table has the following fields: The value of this field should be a string. + +### `TileState` + +Whether a window is tiled or floating. + +Values of this type should be strings. + +The string should have one of the following values: + +- `tiled`: + + The window is tiled. + +- `floating`: + + The window is floating. + + + ### `TransferFunction` @@ -4212,6 +4231,12 @@ The table has the following fields: The value of this field should be a boolean. +- `initial-tile-state` (optional): + + Specifies if the window is initially mapped tiled or floating. + + The value of this field should be a [TileState](#types-TileState). + ### `WindowTypeMask` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 73914d9a..5e2055d6 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -3376,6 +3376,10 @@ WindowRule: If a window matches any rule for which this is false, the window will not be automatically focused. + initial-tile-state: + ref: TileState + required: false + description: Specifies if the window is initially mapped tiled or floating. WindowMatch: @@ -3602,3 +3606,13 @@ WindowTypeMask: description: An array of masks that are OR'd. items: ref: WindowTypeMask + + +TileState: + description: Whether a window is tiled or floating. + kind: string + values: + - value: tiled + description: The window is tiled. + - value: floating + description: The window is floating. From bd04b09171ad2bc1ab21ef4ff80ddbcf71600645 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 6 May 2025 18:12:57 +0200 Subject: [PATCH 33/35] cli: add commands to inspect clients --- src/cli.rs | 10 +- src/cli/clients.rs | 243 +++++++++++++++++++++++++ src/ifs.rs | 1 + src/ifs/jay_client_query.rs | 141 ++++++++++++++ src/ifs/jay_compositor.rs | 53 +++++- src/ifs/jay_select_toplevel.rs | 5 +- src/ifs/jay_toplevel.rs | 10 + src/tools/tool_client.rs | 45 ++++- src/wl_usr/usr_ifs/usr_jay_toplevel.rs | 4 + wire/jay_client_query.txt | 49 +++++ wire/jay_compositor.txt | 8 + wire/jay_toplevel.txt | 4 + 12 files changed, 557 insertions(+), 16 deletions(-) create mode 100644 src/cli/clients.rs create mode 100644 src/ifs/jay_client_query.rs create mode 100644 wire/jay_client_query.txt diff --git a/src/cli.rs b/src/cli.rs index d00b7039..2ebdcd47 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,3 +1,4 @@ +mod clients; mod color; mod color_management; mod damage_tracking; @@ -19,9 +20,9 @@ mod xwayland; use { crate::{ cli::{ - color_management::ColorManagementArgs, damage_tracking::DamageTrackingArgs, - idle::IdleCmd, input::InputArgs, randr::RandrArgs, reexec::ReexecArgs, - xwayland::XwaylandArgs, + clients::ClientsArgs, color_management::ColorManagementArgs, + damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, + reexec::ReexecArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, format::{Format, ref_formats}, @@ -86,6 +87,8 @@ pub enum Cmd { /// Replace the compositor by another process. (Only for development.) #[clap(hide = true)] Reexec(ReexecArgs), + /// Inspect/manipulate the connected clients. + Clients(ClientsArgs), #[cfg(feature = "it")] RunTests, } @@ -244,6 +247,7 @@ pub fn main() { Cmd::DamageTracking(a) => damage_tracking::main(cli.global, a), Cmd::Xwayland(a) => xwayland::main(cli.global, a), Cmd::ColorManagement(a) => color_management::main(cli.global, a), + Cmd::Clients(a) => clients::main(cli.global, a), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), diff --git a/src/cli/clients.rs b/src/cli/clients.rs new file mode 100644 index 00000000..cd6f268a --- /dev/null +++ b/src/cli/clients.rs @@ -0,0 +1,243 @@ +use { + crate::{ + cli::GlobalArgs, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{JayClientQueryId, jay_client_query, jay_compositor}, + }, + ahash::AHashMap, + clap::{Args, Subcommand}, + std::{cell::RefCell, mem, rc::Rc}, + uapi::c, +}; + +#[derive(Args, Debug)] +pub struct ClientsArgs { + #[clap(subcommand)] + cmd: Option, +} + +#[derive(Subcommand, Debug)] +enum ClientsCmd { + /// Show information about clients. + Show(ShowArgs), + /// Disconnect a client. + Kill(KillArgs), +} + +#[derive(Args, Debug)] +struct ShowArgs { + #[clap(subcommand)] + cmd: ShowCmd, +} + +#[derive(Subcommand, Debug)] +enum ShowCmd { + /// Show all clients. + All, + /// Show a client with a given ID. + Id(ShowIdArgs), + /// Interactively select a window and show information about its client. + SelectWindow, +} + +#[derive(Args, Debug)] +struct ShowIdArgs { + /// The ID of the client. + id: u64, +} + +#[derive(Args, Debug)] +struct KillArgs { + #[clap(subcommand)] + cmd: KillCmd, +} + +#[derive(Subcommand, Debug)] +enum KillCmd { + /// Kill the client with a given ID. + Id(KillIdArgs), + /// Interactively select a window and kill its client. + SelectWindow, +} + +#[derive(Args, Debug)] +struct KillIdArgs { + /// The ID of the client. + id: u64, +} + +pub fn main(global: GlobalArgs, clients_args: ClientsArgs) { + with_tool_client(global.log_level.into(), |tc| async move { + let clients = Rc::new(Clients { tc: tc.clone() }); + clients.run(clients_args).await; + }); +} + +struct Clients { + tc: Rc, +} + +impl Clients { + async fn run(&self, args: ClientsArgs) { + let tc = &self.tc; + let comp = tc.jay_compositor().await; + let cmd = args + .cmd + .unwrap_or(ClientsCmd::Show(ShowArgs { cmd: ShowCmd::All })); + match cmd { + ClientsCmd::Show(a) => { + let id = tc.id(); + tc.send(jay_compositor::CreateClientQuery { self_id: comp, id }); + match a.cmd { + ShowCmd::All => { + tc.send(jay_client_query::AddAll { self_id: id }); + } + ShowCmd::Id(a) => { + tc.send(jay_client_query::AddId { + self_id: id, + id: a.id, + }); + } + ShowCmd::SelectWindow => { + let client_id = tc.select_toplevel_client().await; + if client_id == 0 { + fatal!("Did not select a window"); + } + tc.send(jay_client_query::AddId { + self_id: id, + id: client_id, + }); + } + } + tc.send(jay_client_query::Execute { self_id: id }); + let clients = handle_client_query(tc, id).await; + let mut clients = clients.values().collect::>(); + clients.sort_by_key(|c| c.id); + let mut prefix = " ".to_string(); + let mut printer = ClientPrinter { + prefix: &mut prefix, + }; + for client in clients { + println!("- client:"); + printer.print_client(client); + } + } + ClientsCmd::Kill(a) => match a.cmd { + KillCmd::Id(id) => { + tc.send(jay_compositor::KillClient { + self_id: comp, + id: id.id, + }); + } + KillCmd::SelectWindow => { + let client_id = tc.select_toplevel_client().await; + if client_id == 0 { + fatal!("Did not select a window"); + } + tc.send(jay_compositor::KillClient { + self_id: comp, + id: client_id, + }); + } + }, + } + tc.round_trip().await; + } +} + +#[derive(Default)] +pub struct Client { + pub id: u64, + pub sandboxed: bool, + pub sandbox_engine: Option, + pub sandbox_app_id: Option, + pub sandbox_instance_id: Option, + pub uid: Option, + pub pid: Option, + pub is_xwayland: bool, + pub comm: Option, + pub exe: Option, +} + +pub async fn handle_client_query( + tl: &Rc, + id: JayClientQueryId, +) -> AHashMap { + use jay_client_query::*; + let c = Rc::new(RefCell::new(Vec::::new())); + macro_rules! last { + ($c:ident) => { + $c.borrow_mut().last_mut().unwrap() + }; + } + Start::handle(tl, id, c.clone(), |c, event| { + c.borrow_mut().push(Client::default()); + last!(c).id = event.id; + }); + Sandboxed::handle(tl, id, c.clone(), |c, _event| { + last!(c).sandboxed = true; + }); + SandboxEngine::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_engine = Some(event.engine.to_string()); + }); + SandboxAppId::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_app_id = Some(event.app_id.to_string()); + }); + SandboxInstanceId::handle(tl, id, c.clone(), |c, event| { + last!(c).sandbox_instance_id = Some(event.instance_id.to_string()); + }); + Uid::handle(tl, id, c.clone(), |c, event| { + last!(c).uid = Some(event.uid); + }); + Pid::handle(tl, id, c.clone(), |c, event| { + last!(c).pid = Some(event.pid); + }); + IsXwayland::handle(tl, id, c.clone(), |c, _event| { + last!(c).is_xwayland = true; + }); + Comm::handle(tl, id, c.clone(), |c, event| { + last!(c).comm = Some(event.comm.to_string()); + }); + Exe::handle(tl, id, c.clone(), |c, event| { + last!(c).exe = Some(event.exe.to_string()); + }); + tl.round_trip().await; + mem::take(&mut *c.borrow_mut()) + .into_iter() + .map(|c| (c.id, c)) + .collect() +} + +pub struct ClientPrinter<'a> { + pub prefix: &'a mut String, +} + +impl ClientPrinter<'_> { + pub fn print_client(&mut self, c: &Client) { + let p = &self.prefix; + macro_rules! opt { + ($field:ident, $pretty:expr) => { + if let Some(v) = &c.$field { + println!("{p}{}: {}", $pretty, v); + } + }; + } + macro_rules! bol { + ($field:ident, $pretty:expr) => { + if c.$field { + println!("{p}{}", $pretty); + } + }; + } + println!("{p}id: {}", c.id); + bol!(sandboxed, "sandboxed"); + opt!(sandbox_engine, "sandbox engine"); + opt!(sandbox_app_id, "sandbox app id"); + opt!(sandbox_instance_id, "sandbox instance id"); + opt!(uid, "uid"); + opt!(pid, "pid"); + bol!(is_xwayland, "xwayland"); + opt!(comm, "comm"); + opt!(exe, "exe"); + } +} diff --git a/src/ifs.rs b/src/ifs.rs index 9e202ba3..0fb8d916 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -10,6 +10,7 @@ pub mod ext_output_image_capture_source_manager_v1; pub mod ext_session_lock_manager_v1; pub mod ext_session_lock_v1; pub mod ipc; +pub mod jay_client_query; pub mod jay_color_management; pub mod jay_compositor; pub mod jay_damage_tracking; diff --git a/src/ifs/jay_client_query.rs b/src/ifs/jay_client_query.rs new file mode 100644 index 00000000..2a20e109 --- /dev/null +++ b/src/ifs/jay_client_query.rs @@ -0,0 +1,141 @@ +use { + crate::{ + client::{Client, ClientError, ClientId}, + leaks::Tracker, + object::{Object, Version}, + utils::copyhashmap::CopyHashMap, + wire::{ + JayClientQueryId, + jay_client_query::{ + AddAll, AddId, Comm, Destroy, Done, End, Exe, Execute, IsXwayland, + JayClientQueryRequestHandler, Pid, SandboxAppId, SandboxEngine, SandboxInstanceId, + Sandboxed, Start, Uid, + }, + }, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct JayClientQuery { + pub id: JayClientQueryId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + ids: CopyHashMap, + all: Cell, +} + +impl JayClientQuery { + pub fn new(client: &Rc, id: JayClientQueryId, version: Version) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + version, + ids: Default::default(), + all: Cell::new(false), + } + } +} + +impl JayClientQueryRequestHandler for JayClientQuery { + type Error = JayClientQueryError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn execute(&self, _req: Execute, _slf: &Rc) -> Result<(), Self::Error> { + let handle_client = |client: &Rc| { + self.client.event(Start { + self_id: self.id, + id: client.id.raw(), + }); + if !client.is_xwayland { + self.client.event(Uid { + self_id: self.id, + uid: client.pid_info.uid, + }); + self.client.event(Pid { + self_id: self.id, + pid: client.pid_info.pid, + }); + self.client.event(Comm { + self_id: self.id, + comm: &client.pid_info.comm, + }); + self.client.event(Exe { + self_id: self.id, + exe: &client.pid_info.exe, + }); + } + if client.acceptor.sandboxed { + self.client.event(Sandboxed { self_id: self.id }); + } + if client.is_xwayland { + self.client.event(IsXwayland { self_id: self.id }); + } + if let Some(engine) = &client.acceptor.sandbox_engine { + self.client.event(SandboxEngine { + self_id: self.id, + engine, + }); + } + if let Some(app_id) = &client.acceptor.app_id { + self.client.event(SandboxAppId { + self_id: self.id, + app_id, + }); + } + if let Some(instance_id) = &client.acceptor.instance_id { + self.client.event(SandboxInstanceId { + self_id: self.id, + instance_id, + }); + } + self.client.event(End { self_id: self.id }); + }; + if self.all.get() { + for client in self.client.state.clients.clients.borrow().values() { + handle_client(&client.data); + } + } else { + for &id in self.ids.lock().keys() { + let Ok(client) = self.client.state.clients.get(id) else { + continue; + }; + handle_client(&client); + } + } + self.client.event(Done { self_id: self.id }); + Ok(()) + } + + fn add_all(&self, _req: AddAll, _slf: &Rc) -> Result<(), Self::Error> { + self.all.set(true); + Ok(()) + } + + fn add_id(&self, req: AddId, _slf: &Rc) -> Result<(), Self::Error> { + self.ids.set(ClientId::from_raw(req.id), ()); + Ok(()) + } +} + +object_base! { + self = JayClientQuery; + version = self.version; +} + +impl Object for JayClientQuery {} + +simple_add_obj!(JayClientQuery); + +#[derive(Debug, Error)] +pub enum JayClientQueryError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayClientQueryError, ClientError); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 703aea33..b8372e89 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -1,9 +1,10 @@ use { crate::{ cli::CliLogLevel, - client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError}, + client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError, ClientId}, globals::{Global, GlobalName}, ifs::{ + jay_client_query::JayClientQuery, jay_color_management::JayColorManagement, jay_ei_session_builder::JayEiSessionBuilder, jay_idle::JayIdle, @@ -26,7 +27,10 @@ use { object::{Object, Version}, screenshoter::take_screenshot, utils::{errorfmt::ErrorFmt, toplevel_identifier::ToplevelIdentifier}, - wire::{JayCompositorId, JayScreenshotId, jay_compositor::*}, + wire::{ + JayCompositorId, JayScreenshotId, + jay_compositor::{self, *}, + }, }, bstr::ByteSlice, log::Level, @@ -74,7 +78,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 17 + 18 } fn required_caps(&self) -> ClientCaps { @@ -223,7 +227,7 @@ impl JayCompositorRequestHandler for JayCompositor { } fn get_client_id(&self, _req: GetClientId, _slf: &Rc) -> Result<(), Self::Error> { - self.client.event(ClientId { + self.client.event(jay_compositor::ClientId { self_id: self.id, client_id: self.client.id.raw(), }); @@ -367,7 +371,6 @@ impl JayCompositorRequestHandler for JayCompositor { } fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc) -> Result<(), Self::Error> { - let seat = self.client.lookup(req.seat)?; let obj = JaySelectToplevel::new(&self.client, req.id, self.version); track!(self.client, obj); self.client.add_client_obj(&obj)?; @@ -375,12 +378,22 @@ impl JayCompositorRequestHandler for JayCompositor { tl: Default::default(), jst: obj.clone(), }; - seat.global.select_toplevel(selector); + let seat = if req.seat.is_none() { + match self.client.state.seat_queue.last() { + Some(s) => s.deref().clone(), + None => { + obj.done(None); + return Ok(()); + } + } + } else { + self.client.lookup(req.seat)?.global.clone() + }; + seat.select_toplevel(selector); Ok(()) } fn select_workspace(&self, req: SelectWorkspace, _slf: &Rc) -> Result<(), Self::Error> { - let seat = self.client.lookup(req.seat)?; let obj = Rc::new(JaySelectWorkspace { id: req.id, client: self.client.clone(), @@ -393,7 +406,15 @@ impl JayCompositorRequestHandler for JayCompositor { ws: Default::default(), jsw: obj.clone(), }; - seat.global.select_workspace(selector); + let seat = if req.seat.is_none() { + match self.client.state.seat_queue.last() { + Some(s) => s.deref().clone(), + None => return Ok(()), + } + } else { + self.client.lookup(req.seat)?.global.clone() + }; + seat.select_workspace(selector); Ok(()) } @@ -470,6 +491,22 @@ impl JayCompositorRequestHandler for JayCompositor { self.client.add_client_obj(&obj)?; Ok(()) } + + fn create_client_query( + &self, + req: CreateClientQuery, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let obj = Rc::new(JayClientQuery::new(&self.client, req.id, self.version)); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } + + fn kill_client(&self, req: KillClient, _slf: &Rc) -> Result<(), Self::Error> { + self.client.state.clients.kill(ClientId::from_raw(req.id)); + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_select_toplevel.rs b/src/ifs/jay_select_toplevel.rs index 5fb9dec2..9a003b69 100644 --- a/src/ifs/jay_select_toplevel.rs +++ b/src/ifs/jay_select_toplevel.rs @@ -2,7 +2,7 @@ use { crate::{ client::{Client, ClientError}, ifs::{ - jay_toplevel::{ID_SINCE, JayToplevel}, + jay_toplevel::{CLIENT_ID_SINCE, ID_SINCE, JayToplevel}, wl_seat::ToplevelSelector, }, leaks::Tracker, @@ -78,6 +78,9 @@ impl JaySelectToplevel { self.send_done(jtl.id); if jtl.version >= ID_SINCE { jtl.send_id(); + if jtl.version >= CLIENT_ID_SINCE { + jtl.send_client_id(); + } jtl.send_done(); } } diff --git a/src/ifs/jay_toplevel.rs b/src/ifs/jay_toplevel.rs index 38048dc0..9cb6f426 100644 --- a/src/ifs/jay_toplevel.rs +++ b/src/ifs/jay_toplevel.rs @@ -11,6 +11,7 @@ use { }; pub const ID_SINCE: Version = Version(12); +pub const CLIENT_ID_SINCE: Version = Version(18); pub struct JayToplevel { pub id: JayToplevelId, @@ -47,6 +48,15 @@ impl JayToplevel { }) } + pub fn send_client_id(&self) { + if let Some(cl) = &self.toplevel.tl_data().client { + self.client.event(ClientId { + self_id: self.id, + id: cl.id.raw(), + }) + } + } + pub fn send_done(&self) { self.client.event(Done { self_id: self.id }) } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 7c75a8b7..09751475 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -24,7 +24,8 @@ use { wheel::{Wheel, WheelError}, wire::{ JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, WlCallbackId, - WlRegistryId, wl_callback, wl_display, wl_registry, + WlRegistryId, WlSeatId, jay_compositor, jay_select_toplevel, jay_toplevel, wl_callback, + wl_display, wl_registry, }, }, ahash::AHashMap, @@ -63,8 +64,8 @@ pub enum ToolClientError { UnalignedMessage, #[error(transparent)] BufFdError(#[from] BufFdError), - #[error("The size of the message is not a multiple of 4")] - Parsing(&'static str, MsgParserError), + #[error("Could not parse a message of type {}", .0)] + Parsing(&'static str, #[source] MsgParserError), #[error("Could not read from the compositor")] Read(#[source] BufFdError), #[error("Could not write to the compositor")] @@ -195,6 +196,7 @@ impl ToolClient { fatal!("The compositor returned a fatal error: {}", val.message); }); wl_display::DeleteId::handle(&slf, WL_DISPLAY_ID, slf.clone(), |tc, val| { + tc.handlers.borrow_mut().remove(&ObjectId::from_raw(val.id)); tc.obj_ids.borrow_mut().release(val.id); }); slf.incoming.set(Some( @@ -332,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(17), + version: s.jay_compositor.1.min(18), id: id.into(), }); self.jay_compositor.set(Some(id)); @@ -359,6 +361,41 @@ impl ToolClient { self.jay_damage_tracking.set(Some(Some(id))); Some(id) } + + pub async fn select_toplevel_client(self: &Rc) -> u64 { + let id = self.id(); + self.send(jay_compositor::SelectToplevel { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let client_id = Rc::new(Cell::new(0)); + jay_select_toplevel::Done::handle( + self, + id, + (self.clone(), ae.clone(), client_id.clone()), + |(tc, ae, client_id), event| { + if event.id.is_some() { + jay_toplevel::ClientId::handle( + tc, + event.id, + client_id.clone(), + |client_id, event| { + client_id.set(event.id); + }, + ); + jay_toplevel::Done::handle(tc, event.id, ae.clone(), |ae, _event| { + ae.trigger(); + }); + } else { + ae.trigger(); + } + }, + ); + ae.triggered().await; + client_id.get() + } } pub struct Singletons { diff --git a/src/wl_usr/usr_ifs/usr_jay_toplevel.rs b/src/wl_usr/usr_ifs/usr_jay_toplevel.rs index 99ad10f6..bb3b172d 100644 --- a/src/wl_usr/usr_ifs/usr_jay_toplevel.rs +++ b/src/wl_usr/usr_ifs/usr_jay_toplevel.rs @@ -36,6 +36,10 @@ impl JayToplevelEventHandler for UsrJayToplevel { Ok(()) } + fn client_id(&self, _ev: ClientId, _slf: &Rc) -> Result<(), Self::Error> { + Ok(()) + } + fn done(&self, _ev: Done, slf: &Rc) -> Result<(), Self::Error> { if let Some(owner) = self.owner.get() { owner.done(slf); diff --git a/wire/jay_client_query.txt b/wire/jay_client_query.txt new file mode 100644 index 00000000..ef841292 --- /dev/null +++ b/wire/jay_client_query.txt @@ -0,0 +1,49 @@ +request destroy { } + +request execute { } + +request add_all { } + +request add_id { + id: pod(u64), +} + +event done { } + +event start { + id: pod(u64), +} + +event end { } + +event sandboxed { } + +event sandbox_engine { + engine: str, +} + +event sandbox_app_id { + app_id: str, +} + +event sandbox_instance_id { + instance_id: str, +} + +event uid { + uid: pod(uapi::c::uid_t), +} + +event pid { + pid: pod(uapi::c::pid_t), +} + +event is_xwayland { } + +event comm { + comm: str, +} + +event exe { + exe: str, +} diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index 24ec05b4..cecbe1f0 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -109,6 +109,14 @@ request reexec (since = 17) { id: id(jay_reexec), } +request create_client_query (since = 18) { + id: id(jay_client_query), +} + +request kill_client (since = 18) { + id: pod(u64), +} + # events event client_id { diff --git a/wire/jay_toplevel.txt b/wire/jay_toplevel.txt index 2cb50c06..eda692f1 100644 --- a/wire/jay_toplevel.txt +++ b/wire/jay_toplevel.txt @@ -8,5 +8,9 @@ event id (since = 12) { id: str, } +event client_id (since = 18) { + id: pod(u64), +} + event done (since = 12) { } From 38d7a60d0009c6733ff482269a518065124228ae Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 6 May 2025 18:08:14 +0200 Subject: [PATCH 34/35] cli: add commands to inspect the tree --- src/cli.rs | 6 +- src/cli/tree.rs | 419 ++++++++++++++++++ src/ifs.rs | 1 + src/ifs/jay_compositor.rs | 8 + src/ifs/jay_tree_query.rs | 458 ++++++++++++++++++++ src/ifs/wl_surface/xdg_surface.rs | 6 + src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 4 + src/ifs/wl_surface/zwlr_layer_surface_v1.rs | 6 + src/tools/tool_client.rs | 54 ++- src/tree/placeholder.rs | 4 +- src/tree/stacked.rs | 4 + src/tree/toplevel.rs | 4 +- wire/jay_compositor.txt | 4 + wire/jay_tree_query.txt | 102 +++++ 14 files changed, 1072 insertions(+), 8 deletions(-) create mode 100644 src/cli/tree.rs create mode 100644 src/ifs/jay_tree_query.rs create mode 100644 wire/jay_tree_query.txt diff --git a/src/cli.rs b/src/cli.rs index 2ebdcd47..cc31a642 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,7 @@ mod run_privileged; pub mod screenshot; mod seat_test; mod set_log_level; +mod tree; mod unlock; mod xwayland; @@ -22,7 +23,7 @@ use { cli::{ clients::ClientsArgs, color_management::ColorManagementArgs, damage_tracking::DamageTrackingArgs, idle::IdleCmd, input::InputArgs, randr::RandrArgs, - reexec::ReexecArgs, xwayland::XwaylandArgs, + reexec::ReexecArgs, tree::TreeArgs, xwayland::XwaylandArgs, }, compositor::start_compositor, format::{Format, ref_formats}, @@ -89,6 +90,8 @@ pub enum Cmd { Reexec(ReexecArgs), /// Inspect/manipulate the connected clients. Clients(ClientsArgs), + /// Inspect the surface tree. + Tree(TreeArgs), #[cfg(feature = "it")] RunTests, } @@ -248,6 +251,7 @@ pub fn main() { Cmd::Xwayland(a) => xwayland::main(cli.global, a), Cmd::ColorManagement(a) => color_management::main(cli.global, a), Cmd::Clients(a) => clients::main(cli.global, a), + Cmd::Tree(a) => tree::main(cli.global, a), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), Cmd::Reexec(a) => reexec::main(cli.global, a), diff --git a/src/cli/tree.rs b/src/cli/tree.rs new file mode 100644 index 00000000..f751a8e5 --- /dev/null +++ b/src/cli/tree.rs @@ -0,0 +1,419 @@ +use { + crate::{ + cli::{ + GlobalArgs, + clients::{Client, ClientPrinter, handle_client_query}, + }, + ifs::jay_tree_query::{ + TREE_TY_CONTAINER, TREE_TY_DISPLAY, TREE_TY_FLOAT, TREE_TY_LAYER_SURFACE, + TREE_TY_LOCK_SURFACE, TREE_TY_OUTPUT, TREE_TY_PLACEHOLDER, TREE_TY_WORKSPACE, + TREE_TY_X_WINDOW, TREE_TY_XDG_POPUP, TREE_TY_XDG_TOPLEVEL, + }, + rect::Rect, + tools::tool_client::{Handle, ToolClient, with_tool_client}, + wire::{JayCompositorId, JayTreeQueryId, jay_client_query, jay_compositor, jay_tree_query}, + }, + ahash::{AHashMap, AHashSet}, + clap::{Args, Subcommand}, + isnt::std_1::primitive::IsntSliceExt, + std::{cell::RefCell, rc::Rc}, +}; + +#[derive(Args, Debug)] +pub struct TreeArgs { + #[clap(subcommand)] + cmd: TreeCmd, +} + +#[derive(Subcommand, Debug)] +enum TreeCmd { + /// Query the tree. + Query(QueryArgs), +} + +#[derive(Args, Debug)] +struct QueryArgs { + /// Whether to perform a recursive query. + #[arg(short, long)] + recursive: bool, + /// Whether to repeatedly print details of the same client. + #[arg(long)] + all_clients: bool, + #[clap(subcommand)] + cmd: QueryCmd, +} + +#[derive(Subcommand, Debug)] +enum QueryCmd { + /// Query the entire display. + Root, + /// Query a workspace by name. + WorkspaceName(QueryWorkspaceNameArgs), + /// Interactively select a workspace to query. + SelectWorkspace, + /// Interactively select a window to query. + SelectWindow, +} + +#[derive(Args, Debug)] +struct QueryWorkspaceNameArgs { + /// The name of the workspace. + name: String, +} + +pub fn main(global: GlobalArgs, tree_args: TreeArgs) { + with_tool_client(global.log_level.into(), |tc| async move { + let comp = tc.jay_compositor().await; + let tree = Rc::new(Tree { + tc: tc.clone(), + comp, + }); + tree.run(tree_args).await; + }); +} + +struct Tree { + tc: Rc, + comp: JayCompositorId, +} + +impl Tree { + async fn run(&self, args: TreeArgs) { + match &args.cmd { + TreeCmd::Query(a) => self.query(a).await, + } + } + + async fn query(&self, args: &QueryArgs) { + let id = self.tc.id(); + self.tc.send(jay_compositor::CreateTreeQuery { + self_id: self.comp, + id, + }); + let mut query = Query { + tree: self, + tc: &self.tc, + id, + }; + query.run(args).await; + } +} + +struct Query<'a> { + tree: &'a Tree, + tc: &'a Rc, + id: JayTreeQueryId, +} + +#[derive(Debug, Default)] +struct Queried { + not_found: bool, + roots: Vec, + stack: Vec, + client_ids: AHashSet, +} + +#[derive(Debug, Default)] +struct Node { + ty: u32, + children: Vec, + position: Option, + toplevel_id: Option, + client: Option, + title: Option, + app_id: Option, + tag: Option, + x_class: Option, + x_instance: Option, + x_role: Option, + workspace: Option, + placeholder_for: Option, + floating: bool, + visible: bool, + urgent: bool, + fullscreen: bool, + output: Option, +} + +impl Query<'_> { + async fn run(&mut self, args: &QueryArgs) { + match &args.cmd { + QueryCmd::Root => { + self.tc.send(SetRootDisplay { self_id: self.id }); + } + QueryCmd::WorkspaceName(a) => { + self.tc.send(SetRootWorkspaceName { + self_id: self.id, + workspace: &a.name, + }); + } + QueryCmd::SelectWorkspace => { + let id = self.tc.select_workspace().await; + if id.is_none() { + fatal!("Workspace selection failed"); + } + self.tc.send(SetRootWorkspace { + self_id: self.id, + workspace: id, + }); + } + QueryCmd::SelectWindow => { + let id = self.tc.select_toplevel().await; + if id.is_none() { + fatal!("Window selection failed"); + } + self.tc.send(SetRootToplevel { + self_id: self.id, + toplevel: id, + }); + } + } + let tl = self.tc; + let id = self.id; + let d = Rc::new(RefCell::new(Queried::default())); + use jay_tree_query::*; + macro_rules! last { + ($d:ident, $n:ident) => { + let $d = &mut *$d.borrow_mut(); + let $n = $d.stack.last_mut().unwrap(); + }; + } + NotFound::handle(tl, id, d.clone(), |d, _event| { + d.borrow_mut().not_found = true; + }); + End::handle(tl, id, d.clone(), |d, _event| { + let d = &mut *d.borrow_mut(); + let n = d.stack.pop().unwrap(); + if let Some(p) = d.stack.last_mut() { + p.children.push(n); + } else { + d.roots.push(n); + } + }); + Position::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.position = Rect::new_sized(event.x, event.y, event.w, event.h); + }); + Start::handle(tl, id, d.clone(), |d, event| { + let d = &mut *d.borrow_mut(); + let node = Node { + ty: event.ty, + ..Default::default() + }; + d.stack.push(node); + }); + OutputName::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.output = Some(event.name.to_string()); + }); + WorkspaceName::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.workspace = Some(event.name.to_string()); + }); + ToplevelId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.toplevel_id = Some(event.id.to_string()); + }); + ClientId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.client = Some(event.id); + d.client_ids.insert(event.id); + }); + Title::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.title = Some(event.title.to_string()); + }); + AppId::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.app_id = Some(event.app_id.to_string()); + }); + Floating::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.floating = true; + }); + Visible::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.visible = true; + }); + Urgent::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.urgent = true; + }); + Fullscreen::handle(tl, id, d.clone(), |d, _event| { + last!(d, n); + n.fullscreen = true; + }); + Tag::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.tag = Some(event.tag.to_string()); + }); + XClass::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_class = Some(event.class.to_string()); + }); + XInstance::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_instance = Some(event.instance.to_string()); + }); + XRole::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.x_role = Some(event.role.to_string()); + }); + Workspace::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.workspace = Some(event.name.to_string()); + }); + PlaceholderFor::handle(tl, id, d.clone(), |d, event| { + last!(d, n); + n.placeholder_for = Some(event.id.to_string()); + }); + if args.recursive { + tl.send(SetRecursive { + self_id: id, + recursive: 1, + }); + } + tl.send(Execute { self_id: id }); + tl.round_trip().await; + let clients = { + let id = tl.id(); + tl.send(jay_compositor::CreateClientQuery { + self_id: self.tree.comp, + id, + }); + use jay_client_query::*; + for &client in &d.borrow().client_ids { + tl.send(AddId { + self_id: id, + id: client, + }); + } + tl.send(Execute { self_id: id }); + handle_client_query(tl, id).await + }; + let mut printer = Printer { + clients, + printed_clients: Default::default(), + verbose: args.all_clients, + prefix: "".to_string(), + output_depth: 0, + workspace_depth: 0, + }; + for node in &d.borrow().roots { + printer.print(node); + } + } +} + +struct Printer { + clients: AHashMap, + printed_clients: AHashSet, + verbose: bool, + prefix: String, + output_depth: u32, + workspace_depth: u32, +} + +impl Printer { + fn print(&mut self, node: &Node) { + let p = &self.prefix; + 'ty: { + let n = match node.ty { + TREE_TY_DISPLAY => "display", + TREE_TY_OUTPUT => "output", + TREE_TY_WORKSPACE => "workspace", + TREE_TY_FLOAT => "float", + TREE_TY_CONTAINER => "container", + TREE_TY_PLACEHOLDER => "placeholder", + TREE_TY_XDG_TOPLEVEL => "xdg-toplevel", + TREE_TY_X_WINDOW => "x-window", + TREE_TY_XDG_POPUP => "xdg-popup", + TREE_TY_LAYER_SURFACE => "layer-surface", + TREE_TY_LOCK_SURFACE => "lock-surface", + _ => { + println!("{p}- unknown ({}):", node.ty); + break 'ty; + } + }; + println!("{p}- {n}:"); + } + macro_rules! opt { + ($field:ident, $pretty:expr) => { + if let Some(v) = &node.$field { + println!("{p} {}: {}", $pretty, v); + } + }; + } + macro_rules! bol { + ($field:ident, $pretty:expr) => { + if node.$field { + println!("{p} {}", $pretty); + } + }; + } + if node.ty == TREE_TY_OUTPUT { + opt!(output, "name"); + } + if node.ty == TREE_TY_WORKSPACE { + opt!(workspace, "name"); + } + opt!(toplevel_id, "id"); + opt!(placeholder_for, "placeholder-for"); + if let Some(r) = node.position { + println!( + "{p} pos: {}x{} + {}x{}", + r.x1(), + r.y1(), + r.width(), + r.height() + ); + } + if let Some(client_id) = node.client { + let client = self.clients.get(&client_id); + if client.is_some() && (self.printed_clients.insert(client_id) || self.verbose) { + println!("{p} client:"); + let mut prefix = format!("{} ", p); + let mut cp = ClientPrinter { + prefix: &mut prefix, + }; + cp.print_client(client.unwrap()); + } else { + println!("{p} client: {client_id}"); + } + } + opt!(title, "title"); + opt!(app_id, "app-id"); + opt!(tag, "tag"); + opt!(x_class, "x-class"); + opt!(x_instance, "x-instance"); + opt!(x_role, "x-role"); + if self.workspace_depth == 0 && node.ty != TREE_TY_WORKSPACE { + opt!(workspace, "workspace"); + } + if self.workspace_depth == 0 && self.output_depth == 0 && node.ty != TREE_TY_OUTPUT { + opt!(output, "output"); + } + bol!(floating, "floating"); + bol!(visible, "visible"); + bol!(urgent, "urgent"); + bol!(fullscreen, "fullscreen"); + if node.children.is_not_empty() { + let (od, wd) = match node.ty { + TREE_TY_OUTPUT => (1, 0), + TREE_TY_WORKSPACE => (0, 1), + _ => (0, 0), + }; + self.output_depth += od; + self.workspace_depth += wd; + println!("{p} children:"); + let len = self.prefix.len(); + self.prefix.push_str(" "); + for child in &node.children { + self.print(child); + } + self.prefix.truncate(len); + self.output_depth -= od; + self.workspace_depth -= wd; + } + } +} diff --git a/src/ifs.rs b/src/ifs.rs index 0fb8d916..1d50c259 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -31,6 +31,7 @@ pub mod jay_select_toplevel; pub mod jay_select_workspace; pub mod jay_toplevel; pub mod jay_tray_v1; +pub mod jay_tree_query; pub mod jay_workspace; pub mod jay_workspace_watcher; pub mod jay_xwayland; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index b8372e89..70e4e31d 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -20,6 +20,7 @@ use { jay_seat_events::JaySeatEvents, jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector}, jay_select_workspace::{JaySelectWorkspace, JayWorkspaceSelector}, + jay_tree_query::JayTreeQuery, jay_workspace_watcher::JayWorkspaceWatcher, jay_xwayland::JayXwayland, }, @@ -507,6 +508,13 @@ impl JayCompositorRequestHandler for JayCompositor { self.client.state.clients.kill(ClientId::from_raw(req.id)); Ok(()) } + + fn create_tree_query(&self, req: CreateTreeQuery, _slf: &Rc) -> Result<(), Self::Error> { + let obj = Rc::new(JayTreeQuery::new(&self.client, req.id, self.version)); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_tree_query.rs b/src/ifs/jay_tree_query.rs new file mode 100644 index 00000000..1171a89c --- /dev/null +++ b/src/ifs/jay_tree_query.rs @@ -0,0 +1,458 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::GlobalBase, + ifs::wl_surface::{ + ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, + x_surface::xwindow::Xwindow, + xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel}, + zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + }, + leaks::Tracker, + object::{Object, Version}, + rect::Rect, + tree::{ + self, ContainerNode, DisplayNode, FloatNode, Node, NodeVisitor, OutputNode, + PlaceholderNode, ToplevelData, ToplevelNodeBase, ToplevelType, WorkspaceNode, + }, + utils::{opaque::OpaqueError, opt::Opt, toplevel_identifier::ToplevelIdentifier}, + wire::{JayTreeQueryId, jay_tree_query::*}, + }, + isnt::std_1::primitive::IsntStrExt, + std::{ + cell::{Cell, RefCell}, + ops::Deref, + rc::Rc, + str::FromStr, + }, + thiserror::Error, +}; + +pub const TREE_TY_DISPLAY: u32 = 1; +pub const TREE_TY_OUTPUT: u32 = 2; +pub const TREE_TY_WORKSPACE: u32 = 3; +pub const TREE_TY_FLOAT: u32 = 4; +pub const TREE_TY_CONTAINER: u32 = 5; +pub const TREE_TY_PLACEHOLDER: u32 = 6; +pub const TREE_TY_XDG_TOPLEVEL: u32 = 7; +pub const TREE_TY_X_WINDOW: u32 = 8; +pub const TREE_TY_XDG_POPUP: u32 = 9; +pub const TREE_TY_LAYER_SURFACE: u32 = 10; +pub const TREE_TY_LOCK_SURFACE: u32 = 11; + +pub struct JayTreeQuery { + pub id: JayTreeQueryId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + recursive: Cell, + root: RefCell>, +} + +enum Root { + Display, + WorkspaceNode(Rc>), + WorkspaceName(String), + ToplevelId(ToplevelIdentifier), +} + +impl JayTreeQuery { + pub fn new(client: &Rc, id: JayTreeQueryId, version: Version) -> Self { + Self { + id, + client: client.clone(), + tracker: Default::default(), + version, + recursive: Cell::new(false), + root: Default::default(), + } + } + + fn send_node_position(&self, node: &dyn Node) { + let rect = node.node_absolute_position(); + self.send_position(rect); + } + + fn send_position(&self, rect: Rect) { + self.client.event(Position { + self_id: self.id, + x: rect.x1(), + y: rect.y1(), + w: rect.width(), + h: rect.height(), + }); + } + + fn send_not_found(&self) { + self.client.event(NotFound { self_id: self.id }); + } + + fn send_done(&self) { + self.client.event(Done { self_id: self.id }); + } + + fn send_end(&self) { + self.client.event(End { self_id: self.id }); + } + + fn send_start(&self, ty: u32) { + self.client.event(Start { + self_id: self.id, + ty, + }); + } + + fn send_client(&self, node: &impl Node) { + if let Some(id) = node.node_client_id() { + self.client.event(ClientId { + self_id: self.id, + id: id.raw(), + }); + } + } + + fn send_workspace_name(&self, name: &str) { + self.client.event(WorkspaceName { + self_id: self.id, + name, + }); + } + + fn send_output_name(&self, name: &str) { + self.client.event(OutputName { + self_id: self.id, + name, + }); + } + + fn send_toplevel(&self, data: &ToplevelData) { + self.client.event(Start { + self_id: self.id, + ty: match &data.kind { + ToplevelType::Container => TREE_TY_CONTAINER, + ToplevelType::Placeholder(_) => TREE_TY_PLACEHOLDER, + ToplevelType::XdgToplevel(_) => TREE_TY_XDG_TOPLEVEL, + ToplevelType::XWindow(_) => TREE_TY_X_WINDOW, + }, + }); + self.client.event(ToplevelId { + self_id: self.id, + id: &data.identifier.get().to_string(), + }); + self.send_position(data.desired_extents.get()); + if let Some(cl) = data.client.as_ref().map(|c| c.id.raw()) { + self.client.event(ClientId { + self_id: self.id, + id: cl, + }); + } + self.client.event(Title { + self_id: self.id, + title: &data.title.borrow(), + }); + if let Some(w) = data.workspace.get() { + self.send_workspace_name(&w.name); + } + match &data.kind { + ToplevelType::Container => {} + ToplevelType::Placeholder(id) => { + if let Some(id) = *id { + self.client.event(PlaceholderFor { + self_id: self.id, + id: &id.to_string(), + }); + } + } + ToplevelType::XdgToplevel(d) => { + self.client.event(AppId { + self_id: self.id, + app_id: &data.app_id.borrow(), + }); + let tag = &*d.tag.borrow(); + if tag.is_not_empty() { + self.client.event(Tag { + self_id: self.id, + tag, + }); + } + } + ToplevelType::XWindow(d) => { + if let Some(class) = &*d.info.class.borrow() { + self.client.event(XClass { + self_id: self.id, + class, + }); + } + if let Some(instance) = &*d.info.instance.borrow() { + self.client.event(XInstance { + self_id: self.id, + instance, + }); + } + if let Some(role) = &*d.info.role.borrow() { + self.client.event(XRole { + self_id: self.id, + role, + }); + } + } + } + if data.is_floating.get() { + self.client.event(Floating { self_id: self.id }); + } + if data.visible.get() { + self.client.event(Visible { self_id: self.id }); + } + if data.wants_attention.get() { + self.client.event(Urgent { self_id: self.id }); + } + for seat_id in data.seat_foci.lock().keys() { + for seat in data.state.globals.seats.lock().values() { + if seat.id() == *seat_id { + self.client.event(Focused { + self_id: self.id, + global: seat.name().raw(), + }); + } + } + } + if data.is_fullscreen.get() { + self.client.event(Fullscreen { self_id: self.id }); + } + if let Some(ws) = data.workspace.get() { + self.client.event(Workspace { + self_id: self.id, + name: &ws.name, + }); + } + } +} + +impl JayTreeQueryRequestHandler for JayTreeQuery { + type Error = JayTreeQueryError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn execute(&self, _req: Execute, _slf: &Rc) -> Result<(), Self::Error> { + let Some(root) = &*self.root.borrow() else { + return Err(JayTreeQueryError::NoRootSet); + }; + match root { + Root::Display => Visitor(self).visit_display(&self.client.state.root), + Root::WorkspaceNode(n) => match n.get() { + Some(n) => Visitor(self).visit_workspace(&n), + None => self.send_not_found(), + }, + Root::WorkspaceName(n) => match self.client.state.workspaces.get(n) { + Some(n) => Visitor(self).visit_workspace(&n), + None => self.send_not_found(), + }, + Root::ToplevelId(id) => match self + .client + .state + .toplevels + .get(id) + .and_then(|t| t.upgrade()) + { + Some(t) => t.node_visit(&mut Visitor(self)), + None => self.send_not_found(), + }, + } + self.send_done(); + Ok(()) + } + + fn set_root_display(&self, _req: SetRootDisplay, _slf: &Rc) -> Result<(), Self::Error> { + *self.root.borrow_mut() = Some(Root::Display); + Ok(()) + } + + fn set_recursive(&self, req: SetRecursive, _slf: &Rc) -> Result<(), Self::Error> { + self.recursive.set(req.recursive != 0); + Ok(()) + } + + fn set_root_workspace( + &self, + req: SetRootWorkspace, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let ws = self.client.lookup(req.workspace)?; + let opt = match ws.workspace.get() { + Some(ws) => ws.opt.clone(), + _ => Default::default(), + }; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::WorkspaceNode(opt)); + Ok(()) + } + + fn set_root_workspace_name( + &self, + req: SetRootWorkspaceName, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::WorkspaceName(req.workspace.to_owned())); + Ok(()) + } + + fn set_root_toplevel(&self, req: SetRootToplevel, _slf: &Rc) -> Result<(), Self::Error> { + let tl = self.client.lookup(req.toplevel)?; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::ToplevelId(tl.toplevel.tl_data().identifier.get())); + Ok(()) + } + + fn set_root_window_id( + &self, + req: SetRootWindowId<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let id = + ToplevelIdentifier::from_str(req.id).map_err(JayTreeQueryError::InvalidToplevelId)?; + let root = &mut *self.root.borrow_mut(); + *root = Some(Root::ToplevelId(id)); + Ok(()) + } +} + +struct Visitor<'a>(&'a JayTreeQuery); + +impl tree::NodeVisitorBase for Visitor<'_> { + fn visit_container(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_toplevel(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + node.xdg.for_each_popup(|popup| { + NodeVisitor::visit_popup(self, popup); + }); + s.send_end(); + } + + fn visit_popup(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_XDG_POPUP); + s.send_node_position(&**node); + s.send_end(); + } + + fn visit_display(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_DISPLAY); + s.send_node_position(&**node); + if s.recursive.get() { + for output in node.outputs.lock().values() { + NodeVisitor::visit_output(self, output); + } + for stacked in node.stacked.iter() { + if stacked.stacked_has_workspace_link() { + continue; + } + stacked.deref().clone().node_visit(self); + } + } + s.send_end(); + } + + fn visit_output(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_OUTPUT); + s.send_node_position(&**node); + s.send_output_name(&node.global.connector.name); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_float(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_FLOAT); + s.send_node_position(&**node); + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_workspace(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_WORKSPACE); + s.send_node_position(&**node); + s.send_workspace_name(&node.name); + s.send_output_name(&node.output.get().global.connector.name); + for stacked in node.stacked.iter() { + if stacked.stacked_is_xdg_popup() { + continue; + } + stacked.deref().clone().node_visit(self); + } + if s.recursive.get() { + node.node_visit_children(self); + } + s.send_end(); + } + + fn visit_layer_surface(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_LAYER_SURFACE); + s.send_client(&**node); + s.send_node_position(&**node); + node.for_each_popup(|popup| { + NodeVisitor::visit_popup(self, popup); + }); + s.send_end(); + } + + fn visit_xwindow(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + s.send_end(); + } + + fn visit_placeholder(&mut self, node: &Rc) { + let s = self.0; + s.send_toplevel(node.tl_data()); + s.send_end(); + } + + fn visit_lock_surface(&mut self, node: &Rc) { + let s = self.0; + s.send_start(TREE_TY_LOCK_SURFACE); + s.send_client(&**node); + s.send_node_position(&**node); + s.send_end(); + } +} + +object_base! { + self = JayTreeQuery; + version = self.version; +} + +impl Object for JayTreeQuery {} + +simple_add_obj!(JayTreeQuery); + +#[derive(Debug, Error)] +pub enum JayTreeQueryError { + #[error(transparent)] + ClientError(Box), + #[error("Toplevel id is ill-formed")] + InvalidToplevelId(OpaqueError), + #[error("No root node was set")] + NoRootSet, +} +efrom!(JayTreeQueryError, ClientError); diff --git a/src/ifs/wl_surface/xdg_surface.rs b/src/ifs/wl_surface/xdg_surface.rs index d0b9e433..9477c92c 100644 --- a/src/ifs/wl_surface/xdg_surface.rs +++ b/src/ifs/wl_surface/xdg_surface.rs @@ -328,6 +328,12 @@ impl XdgSurface { popup.popup.xdg.set_popup_stack(stack); } } + + pub fn for_each_popup(&self, mut f: impl FnMut(&Rc)) { + for popup in self.popups.lock().values() { + f(&popup.popup); + } + } } impl XdgSurfaceRequestHandler for XdgSurface { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 2fc5685e..7544b529 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -382,6 +382,10 @@ impl StackedNode for XdgPopup { fn stacked_absolute_position_constrains_input(&self) -> bool { false } + + fn stacked_is_xdg_popup(&self) -> bool { + true + } } impl XdgSurfaceExt for XdgPopup { diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 3c759a29..0da67246 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -210,6 +210,12 @@ impl ZwlrLayerSurfaceV1 { m.layer_surface.get_or_insert_default_ext() }) } + + pub fn for_each_popup(&self, mut f: impl FnMut(&Rc)) { + for popup in self.popups.lock().values() { + f(&popup.popup); + } + } } impl ZwlrLayerSurfaceV1RequestHandler for ZwlrLayerSurfaceV1 { diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 09751475..995d240e 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -23,9 +23,10 @@ use { }, wheel::{Wheel, WheelError}, wire::{ - JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, WlCallbackId, - WlRegistryId, WlSeatId, jay_compositor, jay_select_toplevel, jay_toplevel, wl_callback, - wl_display, wl_registry, + JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, JayToplevelId, + JayWorkspaceId, WlCallbackId, WlRegistryId, WlSeatId, jay_compositor, + jay_select_toplevel, jay_select_workspace, jay_toplevel, wl_callback, wl_display, + wl_registry, }, }, ahash::AHashMap, @@ -362,6 +363,53 @@ impl ToolClient { Some(id) } + pub async fn select_workspace(self: &Rc) -> JayWorkspaceId { + let id = self.id(); + self.send(jay_compositor::SelectWorkspace { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let ws = Rc::new(Cell::new(JayWorkspaceId::NONE)); + jay_select_workspace::Cancelled::handle(self, id, ae.clone(), |ae, _event| { + ae.trigger(); + }); + jay_select_workspace::Selected::handle( + self, + id, + (ae.clone(), ws.clone()), + |(ae, ws), event| { + ws.set(event.id); + ae.trigger(); + }, + ); + ae.triggered().await; + ws.get() + } + + pub async fn select_toplevel(self: &Rc) -> JayToplevelId { + let id = self.id(); + self.send(jay_compositor::SelectToplevel { + self_id: self.jay_compositor().await, + id, + seat: WlSeatId::NONE, + }); + let ae = Rc::new(AsyncEvent::default()); + let toplevel = Rc::new(Cell::new(JayToplevelId::NONE)); + jay_select_toplevel::Done::handle( + self, + id, + (ae.clone(), toplevel.clone()), + |(ae, toplevel), event| { + toplevel.set(event.id); + ae.trigger(); + }, + ); + ae.triggered().await; + toplevel.get() + } + pub async fn select_toplevel_client(self: &Rc) -> u64 { let id = self.id(); self.send(jay_compositor::SelectToplevel { diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index 1dd3ee3b..fbf3362c 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -56,7 +56,7 @@ impl PlaceholderNode { state, node.tl_data().title.borrow().clone(), node.node_client(), - ToplevelType::Placeholder, + ToplevelType::Placeholder(Some(node.tl_data().identifier.get())), id, slf, ), @@ -75,7 +75,7 @@ impl PlaceholderNode { state, String::new(), None, - ToplevelType::Placeholder, + ToplevelType::Placeholder(None), id, slf, ), diff --git a/src/tree/stacked.rs b/src/tree/stacked.rs index 4b745005..92caf4d6 100644 --- a/src/tree/stacked.rs +++ b/src/tree/stacked.rs @@ -16,6 +16,10 @@ pub trait StackedNode: Node { fn stacked_absolute_position_constrains_input(&self) -> bool { true } + + fn stacked_is_xdg_popup(&self) -> bool { + false + } } pub trait PinnedNode: StackedNode { diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index adcef45e..161dc3e5 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -279,7 +279,7 @@ impl ToplevelOpt { pub enum ToplevelType { Container, - Placeholder, + Placeholder(Option), XdgToplevel(Rc), XWindow(Rc), } @@ -288,7 +288,7 @@ impl ToplevelType { pub fn to_window_type(&self) -> WindowType { match self { ToplevelType::Container => window::CONTAINER, - ToplevelType::Placeholder => window::PLACEHOLDER, + ToplevelType::Placeholder { .. } => window::PLACEHOLDER, ToplevelType::XdgToplevel { .. } => window::XDG_TOPLEVEL, ToplevelType::XWindow { .. } => window::X_WINDOW, } diff --git a/wire/jay_compositor.txt b/wire/jay_compositor.txt index cecbe1f0..1a4657a8 100644 --- a/wire/jay_compositor.txt +++ b/wire/jay_compositor.txt @@ -117,6 +117,10 @@ request kill_client (since = 18) { id: pod(u64), } +request create_tree_query (since = 18) { + id: id(jay_tree_query), +} + # events event client_id { diff --git a/wire/jay_tree_query.txt b/wire/jay_tree_query.txt new file mode 100644 index 00000000..2919ba46 --- /dev/null +++ b/wire/jay_tree_query.txt @@ -0,0 +1,102 @@ +request destroy { } + +request execute { } + +request set_root_display { } + +request set_recursive { + recursive: u32, +} + +request set_root_workspace { + workspace: id(jay_workspace), +} + +request set_root_workspace_name { + workspace: str, +} + +request set_root_toplevel { + toplevel: id(jay_toplevel), +} + +request set_root_window_id { + id: str, +} + +event done { } + +event not_found { } + +event start { + ty: u32, +} + +event end { } + +event position { + x: i32, + y: i32, + w: i32, + h: i32, +} + +event workspace_name { + name: str, +} + +event output_name { + name: str, +} + +event toplevel_id { + id: str, +} + +event client_id { + id: pod(u64), +} + +event title { + title: str, +} + +event app_id { + app_id: str, +} + +event floating { } + +event visible { } + +event urgent { } + +event focused { + global: u32, +} + +event fullscreen { } + +event tag { + tag: str, +} + +event x_class { + class: str, +} + +event x_instance { + instance: str, +} + +event x_role { + role: str, +} + +event workspace { + name: str, +} + +event placeholder_for { + id: str, +} From 8ab0c8958d0bbca590adfb5dca0ea6a0e99abafc Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 4 May 2025 20:48:57 +0200 Subject: [PATCH 35/35] docs: add release notes --- docs/config.md | 4 + docs/features.md | 10 ++ docs/window-and-client-rules.md | 259 ++++++++++++++++++++++++++++++++ release-notes.md | 4 + 4 files changed, 277 insertions(+) create mode 100644 docs/window-and-client-rules.md diff --git a/docs/config.md b/docs/config.md index 6e53c9c1..b01d8345 100644 --- a/docs/config.md +++ b/docs/config.md @@ -513,3 +513,7 @@ The default configuration will try to start [wl-tray-bridge] to give you access icons and menus. [wl-tray-bridge]: https://github.com/mahkoh/wl-tray-bridge + +### Window and Client Rules + +This is described in [window-and-client-rules.md](window-and-client-rules.md). diff --git a/docs/features.md b/docs/features.md index 918134ba..51924ba8 100644 --- a/docs/features.md +++ b/docs/features.md @@ -49,6 +49,10 @@ Commands: portal Run the desktop portal randr Inspect/modify graphics card and connector settings input Inspect/modify input settings + xwayland Inspect/modify xwayland settings + color-management Inspect/modify the color-management settings + clients Inspect/manipulate the connected clients + tree Inspect the surface tree help Print this message or the help of the given subcommand(s) Options: @@ -138,6 +142,12 @@ Jay uses frame scheduling to achieve input latency as low as 1.5 ms. Jay supports the color management protocol and HDR10. +## Window and Client Rules + +Jay supports powerful window and client rules. + +See [window-and-client-rules.md](window-and-client-rules.md) for more details. + ## Protocol Support Jay supports the following wayland protocols: diff --git a/docs/window-and-client-rules.md b/docs/window-and-client-rules.md new file mode 100644 index 00000000..48b7be77 --- /dev/null +++ b/docs/window-and-client-rules.md @@ -0,0 +1,259 @@ +# Window and Client Rules + +Jay supports powerful window and client rules similar to i3. + +## Example + +```toml +# Move spotify to workspace 3 and fullscreen it. +[[windows]] +match.client.sandbox-app-id = "com.spotify.Client" +action = [ + { type = "move-to-workspace", name = "3" }, + "enter-fullscreen", +] + +# Spawn the Chromium screen sharing window, the GIMP splash screen, and the +# JetBrains splash screen floating and without focus stealing. +[[windows]] +match.any = [ + { title-regex = 'is sharing (your screen|a window)\.$', client.comm = "chromium" }, + { title = "GIMP Startup", app-id = "gimp" }, + { title = "splash", x-class-regex = "^jetbrains-(clion|rustrover)$" } +] +initial-tile-state = "floating" +auto-focus = false + +# Spawn the JetBrains project selector floating. +[[windows]] +match.title-regex = "^Welcome to (RustRover|CLion)$" +match.x-class-regex = "^jetbrains-(clion|rustrover)$" +initial-tile-state = "floating" +``` + +## General Principles + +Each rule consists of three components: + +1. Criteria that determine which clients/windows the rule applies to. +2. An action to execute when a client/window starts matching the rule. +3. An action to execute when a client/window stops matching the rule. + +Each rule can be assigned a name which allows other rules to refer to it. + +Additionally, rules have ad-hoc properties for things that are not easily +expressed via actions, such as whether a window should be mapped floating or +tiled. + +```toml +[[windows]] +name = "..." # the rule name +match = { } # the rule criteria +action = "..." # the action to run on start +latch = "..." # the action to run on stop +``` + +Rules are re-evaluated whenever any of the referenced criteria changes. That is, +if you have the following rule + +```toml +[[windows]] +match.title = "VIM" +action = "enter-fullscreen" +``` + +then the window will enter fullscreen whenever title changes from something that +is not `VIM` to `VIM`. For window rules, if you only want to match windows that +have just been mapped, you can set the `just-mapped` criterion to `true`: + +```toml +[[windows]] +match.title = "VIM" +match.just-mapped = true +action = "enter-fullscreen" +``` + +This is similar to the `initial-title` criterion found in some other +compositors. + +Rules can trigger each other. For example: + +```toml +[[windows]] +match.fullscreen = false +action = "enter-fullscreen" + +[[windows]] +match.fullscreen = true +action = "exit-fullscreen" +``` + +This causes an infinite repetition of switching between windowed and fullscreen. +Jay prevents such loops from locking up the compositor by never performing more +than 1000 action callbacks before yielding to other work. However, they will +still cause the compositor to use 100% CPU and will likely cause affected +clients to be killed, since they won't be able to receive wayland messages fast +enough. + +## Combining Criteria + +Criteria can be combined with the following operations: + +- `any` - match if any of a number of criteria match +- `all` - match if all of a number of criteria match +- `not` - match if a criterion does not match +- `exactly` - match if an exact number of criteria match +- `name` - match if another window rule with that name matches + +```toml +# match windows that have the title `chromium` or `spotify` +match.any = [ + { title = "chromium" }, + { title = "spotify" }, +] + +# match windows whose title match both `chro` and `mium` +match.all = [ + { title-regex = "chro" }, + { title-regex = "mium" }, +] + +# match windows whose title is not `firefox` +match.not.title = "firefox" + +# match windows whose title is `VIM` or whose clients are sandboxed, but not +# both +match.exactly.num = 1 +match.exactly.list = [ + { title = "VIM" }, + { client.sandboxed = true }, +] + +# match if another rule called `another-rule-name` matches +match.name = "another-rule-name" +``` + +A criterion object has multiple fields, for example + +```toml +match.title = "abc" +match.app-id = "xyz" +``` + +These fields are implicitly combined with `all` operator. That is, this behaves +just like + +```toml +match.all = [ + { title = "abc" }, + { app-id = "xyz" }, +] +``` + +## Finding Criteria Values + +To determine which values to use in criteria, the `jay` executable provides the +subcommands `jay clients` and `jay tree` to inspect currently active clients and +open windows. For example + +```text +~$ jay tree query select-window +- xdg-toplevel: + id: 258ae697663a1b8abc7e4da9570ad36f + pos: 1920x36 + 1920x1044 + client: + id: 15 + uid: 1000 + pid: 2159136 + comm: chromium + exe: /usr/lib/chromium/chromium + title: YouTube - Chromium + app-id: chromium + workspace: 2 + visible +``` + +In this case, `select-window` allows you to interactively select a window and +then prints its properties. + +## Client Rules + +```toml +# start executable `b` whenever a client with executable `A` connects +[[clients]] +match.exe = "A" +action = { type = "exec", exec = "b" } +``` + +All properties that can be referred to in client criteria are currently +constant over the lifetime of the client. + +### Client Criteria + +The full specification of client criteria can be found in +[spec.generated.md](../toml-spec/spec/spec.generated.md). + +- `sandboxed` - Matches clients that are/aren't sandboxed. +- `sandbox-engine`, `sandbox-engine-regex` - Matches the sandbox engine that was + used to wrap this client. Usually `org.flatpak`. +- `sandbox-app-id`, `sandbox-app-id-regex` - Matches the app-id provided by the + sandbox engine +- `sandbox-instance-id-id`, `sandbox-instance-id-regex` - Matches the + instance-id provided by the sandbox engine +- `uid`, `pid` - Matches the UID/PID of the client. +- `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. + +## Window Rules + +## Ad-hoc Window Rules + +Rule actions are evaluated asynchronously. For window rules, this means that +they are evaluated after the window has been mapped but before it is displayed +for the first time. This makes them ill-suited for things that need to be fixed +during the mapping process. Ad-hoc window rules can be used to bridge this gap: + +```toml +[[windows]] +match.title = "chromium" +initial-tile-state = "floating" +auto-focus = false +``` + +The `initial-tile-state` rule can be used to define whether the window is mapped +tiled or floating. If no such rule exists, this is determined via heuristics. +If multiple such rules exist and match a window, the compositor picks one at +random. + +The `auto-focus` rule determines if the window is automatically focused when it +is mapped. If no such rule exists, newly mapped windows always get the keyboard +focus except in some cases involving Xwayland. If multiple such rules exist and +match a window, then the window _does not_ get the focus if _any_ of them is set +to `false`. + +## Window Criteria + +The full specification of window criteria can be found in +[spec.generated.md](../toml-spec/spec/spec.generated.md). + +- `types` - Matches the type of a window. Currently there are four types: + containers, placeholders, xdg toplevels, and X windows. If the rule does not + contain such a criterion, the rule will only match windows created by clients, + that is, xdg toplevels and X windows. +- `client` - This is a client criterion. See above. +- `title`, `title-regex` - Matches the title of the window. +- `app-id`, `app-id-regex` - Matches the XDG app-id of the window. +- `floating` - Matches if the window is/isn't floating. +- `visible` - Matches if the window is/isn't visible. +- `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. +- `x-class`, `x-class-regex` - Matches the X class of the window. +- `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. diff --git a/release-notes.md b/release-notes.md index 60dc13e8..86bccd30 100644 --- a/release-notes.md +++ b/release-notes.md @@ -27,6 +27,10 @@ [shortcuts] alt-x = "$switch-to-next" ``` +- Add client and window rules. This is described in detail in + [window-and-client-rules.md](./docs/window-and-client-rules.md). +- Add client and tree CLI subcommands to inspect clients and windows, primarily + to facilitate the writing of window and client rules. # 1.10.0 (2025-04-22)