1
0
Fork 0
forked from wry/wry

Merge pull request #451 from mahkoh/jorth/window-rules

Add client and window rules
This commit is contained in:
mahkoh 2025-05-08 16:46:02 +02:00 committed by GitHub
commit 401e8bb0be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
112 changed files with 10610 additions and 455 deletions

1
Cargo.lock generated
View file

@ -602,6 +602,7 @@ dependencies = [
"pin-project",
"png",
"rand",
"regex",
"repc",
"rustc-demangle",
"serde",

View file

@ -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"

View file

@ -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).

View file

@ -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:

View file

@ -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.

View file

@ -4,7 +4,13 @@ mod logging;
pub(crate) mod string_error;
use {
crate::video::Mode,
crate::{
Workspace,
client::ClientMatcher,
input::Seat,
video::Mode,
window::{WindowMatcher, WindowType},
},
bincode::Options,
serde::{Deserialize, Serialize},
std::marker::PhantomData,
@ -64,3 +70,64 @@ 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<T> {
Matcher(T),
Not(T),
List { list: Vec<T>, all: bool },
Exactly { list: Vec<T>, num: usize },
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum ClientCriterionIpc {
Generic(GenericCriterionIpc<ClientMatcher>),
String {
string: String,
field: ClientCriterionStringField,
regex: bool,
},
Sandboxed,
Uid(i32),
Pid(i32),
IsXwayland,
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum ClientCriterionStringField {
SandboxEngine,
SandboxAppId,
SandboxInstanceId,
Comm,
Exe,
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum WindowCriterionIpc {
Generic(GenericCriterionIpc<WindowMatcher>),
String {
string: String,
field: WindowCriterionStringField,
regex: bool,
},
Types(WindowType),
Client(ClientMatcher),
Floating,
Visible,
Urgent,
SeatFocus(Seat),
Fullscreen,
JustMapped,
Workspace(Workspace),
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum WindowCriterionStringField {
Title,
AppId,
Tag,
XClass,
XInstance,
XRole,
Workspace,
}

View file

@ -3,13 +3,16 @@
use {
crate::{
_private::{
Config, ConfigEntry, ConfigEntryGen, PollableId, VERSION, WireMode, bincode_ops,
ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen,
GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc,
WindowCriterionStringField, WireMode, bincode_ops,
ipc::{
ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource,
},
logging,
},
Axis, Direction, ModifiedKeySym, PciId, Workspace,
client::{Client, ClientCriterion, ClientMatcher, MatchedClient},
exec::Command,
input::{
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile,
@ -29,6 +32,7 @@ use {
Transform, VrrMode,
connector_type::{CON_UNKNOWN, ConnectorType},
},
window::{MatchedWindow, TileState, Window, WindowCriterion, WindowMatcher, WindowType},
xwayland::XScalingMode,
},
bincode::Options,
@ -81,7 +85,7 @@ struct KeyHandler {
latched: Vec<Box<dyn FnOnce()>>,
}
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),
@ -110,10 +114,22 @@ pub(crate) struct Client {
status_task: Cell<Vec<JoinHandle<()>>>,
i3bar_separator: RefCell<Option<Rc<String>>>,
pressed_keysym: Cell<Option<KeySym>>,
client_match_handlers: RefCell<HashMap<ClientMatcher, ClientMatchHandler>>,
window_match_handlers: RefCell<HashMap<WindowMatcher, WindowMatchHandler>>,
feat_mod_mask: Cell<bool>,
}
struct ClientMatchHandler {
cb: Callback<MatchedClient>,
latched: HashMap<Client, Box<dyn FnOnce()>>,
}
struct WindowMatchHandler {
cb: Callback<MatchedWindow>,
latched: HashMap<Window, Box<dyn FnOnce()>>,
}
struct Interest {
result: Option<Result<(), String>>,
waker: Option<Waker>,
@ -145,7 +161,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 +170,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, F: FnOnce(&Client) -> T>(data: *const u8, f: F) -> T {
unsafe fn with_client<T, F: FnOnce(&ConfigClient) -> 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 +184,7 @@ unsafe fn with_client<T, F: FnOnce(&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 +230,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,
@ -243,6 +259,8 @@ pub unsafe extern "C" fn init(
status_task: Default::default(),
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) };
@ -251,7 +269,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 +296,17 @@ macro_rules! get_response {
}
}
impl Client {
#[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();
buf.clear();
@ -333,12 +361,80 @@ 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 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<Window> {
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<T: Into<ModifiedKeySym>>(&self, seat: Seat, mod_sym: T) {
@ -367,12 +463,18 @@ 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
}
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 });
@ -420,6 +522,12 @@ impl Client {
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 });
@ -450,16 +558,26 @@ 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 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 });
}
@ -471,16 +589,26 @@ 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
}
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);
}
@ -495,18 +623,28 @@ 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 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) {
@ -548,8 +686,12 @@ 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_window_mono(&self, window: Window, mono: bool) {
self.send(&ClientMessage::SetWindowMono { window, mono });
}
pub fn set_env(&self, key: &str, val: &str) {
@ -582,20 +724,32 @@ 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 set_window_split(&self, window: Window, axis: Axis) {
self.send(&ClientMessage::SetWindowSplit { window, axis });
}
pub fn close(&self, seat: Seat) {
self.send(&ClientMessage::Close { seat });
pub fn create_seat_split(&self, seat: Seat, axis: Axis) {
self.send(&ClientMessage::CreateSeatSplit { seat, axis });
}
pub fn focus_parent(&self, seat: Seat) {
self.send(&ClientMessage::FocusParent { seat });
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 });
}
pub fn get_seat(&self, name: &str) -> Seat {
@ -792,13 +946,23 @@ 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 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 {
@ -1259,6 +1423,319 @@ impl Client {
}
}
pub fn clients(&self) -> Vec<Client> {
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 create_generic_matcher<Crit, Matcher>(
&self,
criterion: GenericCriterion<'_, Crit, Matcher>,
child: bool,
create_child_matcher: impl Fn(Crit) -> (Matcher, bool),
create_matcher: impl Fn(GenericCriterionIpc<Matcher>) -> 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) {
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<ClientCriterion, ClientMatcher>| {
self.create_generic_matcher::<ClientCriterion, ClientMatcher>(
crit,
child,
|c| self.create_client_matcher_(c, true),
create_matcher,
destroy_matcher,
)
};
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,
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),
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!(
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);
}
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) {
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<WindowCriterion, WindowMatcher>| {
self.create_generic_matcher(
crit,
child,
|c| self.create_window_matcher_(c, true),
create_matcher,
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)),
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),
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)
}
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),
WindowCriterion::Floating => WindowCriterionIpc::Floating,
WindowCriterion::Visible => WindowCriterionIpc::Visible,
WindowCriterion::Urgent => WindowCriterionIpc::Urgent,
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),
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),
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!(
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_auto_focus(&self, matcher: WindowMatcher, auto_focus: bool) {
self.send(&ClientMessage::SetWindowMatcherAutoFocus {
matcher,
auto_focus,
});
}
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,
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();
@ -1521,6 +1998,54 @@ impl Client {
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();
}
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();
}
}
}

View file

@ -1,7 +1,8 @@
use {
crate::{
_private::{PollableId, WireMode},
_private::{ClientCriterionIpc, PollableId, WindowCriterionIpc, WireMode},
Axis, Direction, PciId, Workspace,
client::{Client, ClientMatcher},
input::{
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile,
capability::Capability,
@ -14,6 +15,7 @@ use {
ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction,
Transform, VrrMode, connector_type::ConnectorType,
},
window::{TileState, Window, WindowMatcher, WindowType},
xwayland::XScalingMode,
},
serde::{Deserialize, Serialize},
@ -92,6 +94,22 @@ pub enum ServerMessage {
input_device: InputDevice,
event: SwitchEvent,
},
ClientMatcherMatched {
matcher: ClientMatcher,
client: Client,
},
ClientMatcherUnmatched {
matcher: ClientMatcher,
client: Client,
},
WindowMatcherMatched {
matcher: WindowMatcher,
window: Window,
},
WindowMatcherUnmatched {
matcher: WindowMatcher,
window: Window,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -129,20 +147,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 +186,11 @@ pub enum ClientMessage<'a> {
args: Vec<String>,
env: Vec<(String, String)>,
},
Focus {
SeatFocus {
seat: Seat,
direction: Direction,
},
Move {
SeatMove {
seat: Seat,
direction: Direction,
},
@ -196,20 +214,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 +279,7 @@ pub enum ClientMessage<'a> {
seat: Seat,
workspace: Workspace,
},
SetWorkspace {
SetSeatWorkspace {
seat: Seat,
workspace: Workspace,
},
@ -280,11 +298,11 @@ pub enum ClientMessage<'a> {
key: &'a str,
val: &'a str,
},
SetFullscreen {
SetSeatFullscreen {
seat: Seat,
fullscreen: bool,
},
GetFullscreen {
GetSeatFullscreen {
seat: Seat,
},
GetDeviceConnectors {
@ -546,10 +564,10 @@ pub enum ClientMessage<'a> {
above: bool,
},
GetFloatAboveFullscreen,
GetFloatPinned {
GetSeatFloatPinned {
seat: Seat,
},
SetFloatPinned {
SetSeatFloatPinned {
seat: Seat,
pinned: bool,
},
@ -565,6 +583,129 @@ pub enum ClientMessage<'a> {
GetConnectorWorkspaces {
connector: Connector,
},
GetClients,
ClientExists {
client: Client,
},
ClientKill {
client: Client,
},
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,
},
CreateClientMatcher {
criterion: ClientCriterionIpc,
},
DestroyClientMatcher {
matcher: ClientMatcher,
},
EnableClientMatcherEvents {
matcher: ClientMatcher,
},
CreateWindowMatcher {
criterion: WindowCriterionIpc,
},
DestroyWindowMatcher {
matcher: WindowMatcher,
},
EnableWindowMatcherEvents {
matcher: WindowMatcher,
},
SetWindowMatcherAutoFocus {
matcher: WindowMatcher,
auto_focus: bool,
},
SetWindowMatcherInitialTileState {
matcher: WindowMatcher,
tile_state: TileState,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -728,6 +869,69 @@ pub enum Response {
GetConnectorWorkspaces {
workspaces: Vec<Workspace>,
},
GetClients {
clients: Vec<Client>,
},
ClientExists {
exists: bool,
},
ClientIsXwayland {
is_xwayland: bool,
},
WindowExists {
exists: bool,
},
GetWindowClient {
client: Client,
},
GetSeatKeyboardWindow {
window: Window,
},
GetWorkspaceWindow {
window: Window,
},
GetWindowParent {
window: Window,
},
GetWindowChildren {
windows: Vec<Window>,
},
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,
},
CreateClientMatcher {
matcher: ClientMatcher,
},
CreateWindowMatcher {
matcher: WindowMatcher,
},
}
#[derive(Serialize, Deserialize, Debug)]

149
jay-config/src/client.rs Normal file
View file

@ -0,0 +1,149 @@
//! Tools for inspecting and manipulating clients.
use {
serde::{Deserialize, Serialize},
std::ops::Deref,
};
/// 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<Client> {
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>]),
/// 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,
/// Matches the user ID of the client.
Uid(i32),
/// Matches the process ID of the client.
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),
/// 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<'_> {
/// 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<F: FnMut(MatchedClient) + 'static>(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<F: FnMut(MatchedClient) + 'static>(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<F: FnOnce() + 'static>(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
}
}

View file

@ -10,6 +10,7 @@ use {
input::{acceleration::AccelProfile, capability::Capability},
keyboard::{Keymap, mods::Modifiers},
video::Connector,
window::Window,
},
serde::{Deserialize, Serialize},
std::time::Duration,
@ -259,12 +260,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 +288,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 +303,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 +323,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 +378,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.
@ -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.

View file

@ -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<KeySym> for Modifiers {
type Output = ModifiedKeySym;
@ -64,23 +58,3 @@ impl BitOr<KeySym> 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
}
}

View file

@ -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},
@ -58,6 +60,7 @@ use {
mod macros;
#[doc(hidden)]
pub mod _private;
pub mod client;
pub mod embedded;
pub mod exec;
pub mod input;
@ -69,6 +72,7 @@ pub mod tasks;
pub mod theme;
pub mod timer;
pub mod video;
pub mod window;
pub mod xwayland;
/// A planar direction.
@ -173,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.

View file

@ -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(())
}
}
}
}

388
jay-config/src/window.rs Normal file
View file

@ -0,0 +1,388 @@
//! Tools for inspecting and manipulating windows.
use {
crate::{
Axis, Direction, Workspace,
client::{Client, ClientCriterion},
input::Seat,
},
serde::{Deserialize, Serialize},
std::ops::Deref,
};
/// 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,
}
}
/// 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`.
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<Window> {
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());
}
}
/// 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>]),
/// 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),
/// 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),
/// Matches if the window is floating.
Floating,
/// Matches if the window is visible.
Visible,
/// Matches if the window has the urgency flag set.
Urgent,
/// Matches if the window has the keyboard focus of the seat.
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,
/// 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),
/// 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),
/// 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),
/// 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<'_> {
/// 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<F: FnMut(MatchedWindow) + 'static>(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);
}
/// 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 {
/// 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<F: FnMut(MatchedWindow) + 'static>(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);
}
/// 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 {
/// 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<F: FnOnce() + 'static>(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
}
}

View file

@ -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)

View file

@ -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<OwnedFd>, state: Rc<State>, 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<OwnedFd>, state: Rc<State>, 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;

View file

@ -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()
}

View file

@ -1,3 +1,4 @@
mod clients;
mod color;
mod color_management;
mod damage_tracking;
@ -13,15 +14,16 @@ mod run_privileged;
pub mod screenshot;
mod seat_test;
mod set_log_level;
mod tree;
mod unlock;
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, tree::TreeArgs, xwayland::XwaylandArgs,
},
compositor::start_compositor,
format::{Format, ref_formats},
@ -86,6 +88,10 @@ pub enum Cmd {
/// Replace the compositor by another process. (Only for development.)
#[clap(hide = true)]
Reexec(ReexecArgs),
/// Inspect/manipulate the connected clients.
Clients(ClientsArgs),
/// Inspect the surface tree.
Tree(TreeArgs),
#[cfg(feature = "it")]
RunTests,
}
@ -244,6 +250,8 @@ 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),
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),

243
src/cli/clients.rs Normal file
View file

@ -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<ClientsCmd>,
}
#[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<ToolClient>,
}
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::<Vec<_>>();
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<String>,
pub sandbox_app_id: Option<String>,
pub sandbox_instance_id: Option<String>,
pub uid: Option<c::uid_t>,
pub pid: Option<c::pid_t>,
pub is_xwayland: bool,
pub comm: Option<String>,
pub exe: Option<String>,
}
pub async fn handle_client_query(
tl: &Rc<ToolClient>,
id: JayClientQueryId,
) -> AHashMap<u64, Client> {
use jay_client_query::*;
let c = Rc::new(RefCell::new(Vec::<Client>::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");
}
}

419
src/cli/tree.rs Normal file
View file

@ -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<ToolClient>,
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<ToolClient>,
id: JayTreeQueryId,
}
#[derive(Debug, Default)]
struct Queried {
not_found: bool,
roots: Vec<Node>,
stack: Vec<Node>,
client_ids: AHashSet<u64>,
}
#[derive(Debug, Default)]
struct Node {
ty: u32,
children: Vec<Node>,
position: Option<Rect>,
toplevel_id: Option<String>,
client: Option<u64>,
title: Option<String>,
app_id: Option<String>,
tag: Option<String>,
x_class: Option<String>,
x_instance: Option<String>,
x_role: Option<String>,
workspace: Option<String>,
placeholder_for: Option<String>,
floating: bool,
visible: bool,
urgent: bool,
fullscreen: bool,
output: Option<String>,
}
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<u64, Client>,
printed_clients: AHashSet<u64>,
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;
}
}
}

View file

@ -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,
@ -9,6 +13,7 @@ use {
},
leaks::Tracker,
object::{Interface, Object, ObjectId, WL_DISPLAY_ID},
security_context_acceptor::AcceptorMetadata,
state::State,
utils::{
activation_token::ActivationToken,
@ -19,6 +24,7 @@ use {
numcell::NumCell,
pending_serial::PendingSerial,
pid_info::{PidInfo, get_pid_info, get_socket_creds},
pidfd_send_signal::pidfd_send_signal,
},
wire::WlRegistryId,
},
@ -30,7 +36,7 @@ use {
fmt::{Debug, Display, Formatter},
mem,
ops::DerefMut,
rc::Rc,
rc::{Rc, Weak},
},
uapi::{OwnedFd, c},
};
@ -105,7 +111,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<Rc<Client>, ClientError> {
let clients = self.clients.borrow();
match clients.get(&id) {
@ -121,6 +126,7 @@ impl Clients {
socket: Rc<OwnedFd>,
effective_caps: ClientCaps,
bounding_caps: ClientCaps,
acceptor: &Rc<AcceptorMetadata>,
) -> Result<(), ClientError> {
let Some((uid, pid)) = get_socket_creds(&socket) else {
return Ok(());
@ -134,6 +140,7 @@ impl Clients {
effective_caps,
bounding_caps,
false,
acceptor,
)?;
Ok(())
}
@ -148,6 +155,7 @@ impl Clients {
effective_caps: ClientCaps,
bounding_caps: ClientCaps,
is_xwayland: bool,
acceptor: &Rc<AcceptorMetadata>,
) -> Result<Rc<Client>, ClientError> {
let data = Rc::new_cyclic(|slf| Client {
id,
@ -177,6 +185,9 @@ impl Clients {
)),
wire_scale: Default::default(),
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));
@ -196,6 +207,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 +263,14 @@ 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) {
log::error!("Could not kill Xwayland: {}", ErrorFmt(e));
}
}
}
}
}
@ -289,6 +309,9 @@ pub struct Client {
pub commit_timelines: Rc<CommitTimelines>,
pub wire_scale: Cell<Option<i32>>,
pub focus_stealing_serial: Cell<Option<u64>>,
pub changed_properties: Cell<ClMatcherChange>,
pub destroyed: CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<Rc<Self>>>>,
pub acceptor: Rc<AcceptorMetadata>,
}
pub const NUM_CACHED_SERIAL_RANGES: usize = 64;
@ -494,6 +517,14 @@ impl Client {
self.state.activation_tokens.remove(token);
}
}
pub fn property_changed(self: &Rc<Self>, 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 {

View file

@ -15,6 +15,13 @@ 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},
tlm::{
TlMatcherManager, handle_tl_changes, handle_tl_just_mapped, handle_tl_leaf_events,
},
},
damage::{DamageVisualizer, visualize_damage},
dbus::Dbus,
ei::ei_client::EiClients,
@ -156,6 +163,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)),
@ -218,11 +226,13 @@ 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(),
use_wire_scale: Default::default(),
wire_scale: Default::default(),
windows: Default::default(),
},
acceptor: Default::default(),
serial: Default::default(),
@ -292,6 +302,8 @@ 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),
tl_matcher_manager: TlMatcherManager::new(&crit_ids),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
@ -464,6 +476,21 @@ 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()),
),
eng.spawn("tl matcher manager", handle_tl_changes(state.clone())),
eng.spawn(
"tl matcher leaf events",
handle_tl_leaf_events(state.clone()),
),
eng.spawn2(
"tl matcher just mapped",
Phase::Layout,
handle_tl_just_mapped(state.clone()),
),
]
}

View file

@ -8,9 +8,10 @@ use {
config::handler::ConfigProxyHandler,
ifs::wl_seat::SeatId,
state::State,
tree::ToplevelData,
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,
@ -22,6 +23,7 @@ use {
input::{InputDevice, Seat, SwitchEvent},
keyboard::{mods::Modifiers, syms::KeySym},
video::{Connector, DrmDevice},
window::{self, TileState},
},
libloading::Library,
std::{cell::Cell, io, mem, ptr, rc::Rc},
@ -151,6 +153,26 @@ 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);
}
}
pub fn auto_focus(&self, data: &ToplevelData) -> bool {
let Some(handler) = self.handler.get() else {
return true;
};
handler.auto_focus(data)
}
pub fn initial_tile_state(&self, data: &ToplevelData) -> Option<TileState> {
self.handler.get()?.initial_tile_state(data)
}
}
impl Drop for ConfigProxy {
@ -202,6 +224,20 @@ 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(),
client_matcher_ids: NumCell::new(1),
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),
window_matcher_no_auto_focus: Default::default(),
window_matcher_initial_tile_state: Default::default(),
});
let init_msg = bincode_ops()
.serialize(&InitMessage::V1(V1InitMessage {}))

File diff suppressed because it is too large Load diff

96
src/criteria.rs Normal file
View file

@ -0,0 +1,96 @@
pub mod clm;
mod crit_graph;
pub mod crit_leaf;
mod crit_matchers;
mod crit_per_target_data;
pub mod tlm;
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<Target, T> = CopyHashMap<CritMatcherId, Weak<CritRoot<Target, T>>>;
type FixedRootMatcher<Target, T> = StaticMap<bool, Rc<CritRoot<Target, CritRootFixed<Target, T>>>>;
#[derive(Clone)]
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),
}
}
}
pub trait CritMgrExt: CritMgr {
fn list(
&self,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
all: bool,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
if upstream.is_empty() {
return self.match_constant()[all].clone();
}
CritMiddle::new(self, upstream, CritMatchAnyOrAll::new(upstream, all))
}
fn exactly(
&self,
upstream: &[Rc<dyn CritUpstreamNode<Self::Target>>],
num: usize,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
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<dyn CritUpstreamNode<Self::Target>>,
on_match: impl Fn(<Self::Target as CritTarget>::LeafData) -> Box<dyn FnOnce()> + 'static,
) -> Rc<CritLeafMatcher<Self::Target>> {
CritLeafMatcher::new(self, upstream, on_match)
}
fn not(
&self,
upstream: &Rc<dyn CritUpstreamNode<Self::Target>>,
) -> Rc<dyn CritUpstreamNode<Self::Target>> {
upstream.not(self)
}
fn root<T>(&self, criterion: T) -> Rc<dyn CritUpstreamNode<Self::Target>>
where
T: CritRootCriterion<Self::Target>,
{
CritRoot::new(self.roots(), self.id(), criterion)
}
}
impl<T> CritMgrExt for T where T: CritMgr {}

270
src/criteria/clm.rs Normal file
View file

@ -0,0 +1,270 @@
pub mod clm_matchers;
use {
crate::{
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt,
CritUpstreamNode, FixedRootMatcher, RootMatcherMap,
clm::clm_matchers::{
clmm_is_xwayland::ClmMatchIsXwayland,
clmm_pid::ClmMatchPid,
clmm_sandboxed::ClmMatchSandboxed,
clmm_string::{
ClmMatchComm, ClmMatchExe, ClmMatchSandboxAppId, ClmMatchSandboxEngine,
ClmMatchSandboxInstanceId,
},
clmm_uid::ClmMatchUid,
},
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},
},
linearize::static_map,
std::{
marker::PhantomData,
rc::{Rc, Weak},
},
};
bitflags! {
ClMatcherChange: u32;
CL_CHANGED_DESTROYED = 1 << 0,
CL_CHANGED_NEW = 1 << 1,
}
type ClmFixedRootMatcher<T> = FixedRootMatcher<Rc<Client>, T>;
pub struct ClMatcherManager {
ids: Rc<CritMatcherIds>,
changes: AsyncQueue<Rc<Client>>,
leaf_events: Rc<AsyncQueue<CritLeafEvent<Rc<Client>>>>,
constant: ClmFixedRootMatcher<CritMatchConstant<Rc<Client>>>,
sandboxed: ClmFixedRootMatcher<ClmMatchSandboxed>,
is_xwayland: ClmFixedRootMatcher<ClmMatchIsXwayland>,
matchers: Rc<RootMatchers>,
}
type ClmRootMatcherMap<T> = RootMatcherMap<Rc<Client>, T>;
#[derive(Default)]
pub struct RootMatchers {
sandbox_app_id: ClmRootMatcherMap<ClmMatchSandboxAppId>,
sandbox_engine: ClmRootMatcherMap<ClmMatchSandboxEngine>,
sandbox_instance_id: ClmRootMatcherMap<ClmMatchSandboxInstanceId>,
uid: ClmRootMatcherMap<ClmMatchUid>,
pid: ClmRootMatcherMap<ClmMatchPid>,
comm: ClmRootMatcherMap<ClmMatchComm>,
exe: ClmRootMatcherMap<ClmMatchExe>,
}
pub async fn handle_cl_changes(state: Rc<State>) {
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<State>) {
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;
}
}
pub type ClmUpstreamNode = dyn CritUpstreamNode<Rc<Client>>;
pub type ClmLeafMatcher = CritLeafMatcher<Rc<Client>>;
impl ClMatcherManager {
pub fn new(ids: &Rc<CritMatcherIds>) -> 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),
sandboxed: bool!(ClmMatchSandboxed),
is_xwayland: bool!(ClmMatchIsXwayland),
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<State>) {
for client in state.clients.clients.borrow().values() {
client.data.property_changed(CL_CHANGED_NEW);
}
}
pub fn changed(&self, client: &Rc<Client>) {
self.changes.push(client.clone());
}
fn update_matches(&self, data: &Rc<Client>) {
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;
}
macro_rules! handlers {
($name:ident) => {
self.matchers
.$name
.lock()
.values()
.filter_map(|m| m.upgrade())
};
}
macro_rules! fixed {
($name:ident) => {
self.$name[false].handle(data);
self.$name[true].handle(data);
};
}
if changed.contains(CL_CHANGED_NEW) {
changed |= ClMatcherChange::all();
macro_rules! unconditional {
($field:ident) => {
for m in handlers!($field) {
m.handle(data);
}
};
}
unconditional!(sandbox_instance_id);
unconditional!(sandbox_app_id);
unconditional!(sandbox_engine);
unconditional!(uid);
unconditional!(pid);
unconditional!(comm);
unconditional!(exe);
fixed!(sandboxed);
fixed!(is_xwayland);
self.constant[true].handle(data);
}
}
pub fn sandbox_engine(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchSandboxEngine::new(string))
}
pub fn sandbox_app_id(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchSandboxAppId::new(string))
}
pub fn sandbox_instance_id(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchSandboxInstanceId::new(string))
}
pub fn sandboxed(&self) -> Rc<ClmUpstreamNode> {
self.sandboxed[true].clone()
}
pub fn uid(&self, pid: i32) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchUid(pid as _))
}
pub fn pid(&self, pid: i32) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchPid(pid as _))
}
pub fn is_xwayland(&self) -> Rc<ClmUpstreamNode> {
self.is_xwayland[true].clone()
}
pub fn comm(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchComm::new(string))
}
pub fn exe(&self, string: CritLiteralOrRegex) -> Rc<ClmUpstreamNode> {
self.root(ClmMatchExe::new(string))
}
}
impl CritTarget for Rc<Client> {
type Id = ClientId;
type Mgr = ClMatcherManager;
type RootMatchers = RootMatchers;
type LeafData = ClientId;
type Owner = Weak<Client>;
fn owner(&self) -> Self::Owner {
Rc::downgrade(self)
}
fn id(&self) -> Self::Id {
self.id
}
fn destroyed(&self) -> &CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<Self>>> {
&self.destroyed
}
fn leaf_data(&self) -> Self::LeafData {
self.id
}
}
impl CritTargetOwner for Rc<Client> {
type Target = Rc<Client>;
fn data(&self) -> &Self::Target {
self
}
}
impl WeakCritTargetOwner for Weak<Client> {
type Target = Rc<Client>;
type Owner = Rc<Client>;
fn upgrade(&self) -> Option<Self::Owner> {
self.upgrade()
}
}
impl CritMgr for ClMatcherManager {
type Target = Rc<Client>;
fn id(&self) -> CritMatcherId {
self.ids.next()
}
fn leaf_events(&self) -> &Rc<AsyncQueue<CritLeafEvent<Self::Target>>> {
&self.leaf_events
}
fn match_constant(&self) -> &FixedRootMatcher<Self::Target, CritMatchConstant<Self::Target>> {
&self.constant
}
fn roots(&self) -> &Rc<<Self::Target as CritTarget>::RootMatchers> {
&self.matchers
}
}

View file

@ -0,0 +1,24 @@
macro_rules! fixed_root_criterion {
($ty:ty, $field:ident) => {
impl crate::criteria::crit_graph::CritFixedRootCriterionBase<Rc<crate::client::Client>>
for $ty
{
fn constant(&self) -> bool {
self.0
}
fn not<'a>(
&self,
mgr: &'a crate::criteria::clm::ClMatcherManager,
) -> &'a crate::criteria::FixedRootMatcher<Rc<crate::client::Client>, Self> {
&mgr.$field
}
}
};
}
pub mod clmm_is_xwayland;
pub mod clmm_pid;
pub mod clmm_sandboxed;
pub mod clmm_string;
pub mod clmm_uid;

View file

@ -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<Rc<Client>> for ClmMatchIsXwayland {
fn matches(&self, data: &Rc<Client>) -> bool {
data.is_xwayland
}
}

View file

@ -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<Rc<Client>> for ClmMatchPid {
fn matches(&self, data: &Rc<Client>) -> bool {
data.pid_info.pid == self.0
}
fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap<Rc<Client>, Self>> {
Some(&roots.pid)
}
}

View file

@ -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<Rc<Client>> for ClmMatchSandboxed {
fn matches(&self, data: &Rc<Client>) -> bool {
data.acceptor.sandboxed
}
}

View file

@ -0,0 +1,103 @@
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<T> = CritMatchString<Rc<Client>, T>;
pub type ClmMatchSandboxEngine = ClmMatchString<AcceptorMetadataAccess<SandboxEngineField>>;
pub type ClmMatchSandboxAppId = ClmMatchString<AcceptorMetadataAccess<SandboxAppIdField>>;
pub type ClmMatchSandboxInstanceId = ClmMatchString<AcceptorMetadataAccess<SandboxInstanceIdField>>;
pub type ClmMatchComm = ClmMatchString<CommAccess>;
pub type ClmMatchExe = ClmMatchString<ExeAccess>;
pub struct AcceptorMetadataAccess<T>(PhantomData<T>);
pub struct CommAccess;
pub struct ExeAccess;
trait SandboxField: Sized + 'static {
fn field(meta: &AcceptorMetadata) -> &Option<String>;
fn nodes(
roots: &RootMatchers,
) -> &ClmRootMatcherMap<ClmMatchString<AcceptorMetadataAccess<Self>>>;
}
pub struct SandboxEngineField;
pub struct SandboxAppIdField;
pub struct SandboxInstanceIdField;
impl<T> StringAccess<Rc<Client>> for AcceptorMetadataAccess<T>
where
T: SandboxField,
{
fn with_string(data: &Rc<Client>, f: impl FnOnce(&str) -> bool) -> bool {
f(T::field(&data.acceptor).as_deref().unwrap_or_default())
}
fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap<ClmMatchString<Self>> {
T::nodes(roots)
}
}
impl SandboxField for SandboxEngineField {
fn field(meta: &AcceptorMetadata) -> &Option<String> {
&meta.sandbox_engine
}
fn nodes(
roots: &RootMatchers,
) -> &ClmRootMatcherMap<ClmMatchString<AcceptorMetadataAccess<Self>>> {
&roots.sandbox_engine
}
}
impl SandboxField for SandboxAppIdField {
fn field(meta: &AcceptorMetadata) -> &Option<String> {
&meta.app_id
}
fn nodes(
roots: &RootMatchers,
) -> &ClmRootMatcherMap<ClmMatchString<AcceptorMetadataAccess<Self>>> {
&roots.sandbox_app_id
}
}
impl SandboxField for SandboxInstanceIdField {
fn field(meta: &AcceptorMetadata) -> &Option<String> {
&meta.instance_id
}
fn nodes(
roots: &RootMatchers,
) -> &ClmRootMatcherMap<ClmMatchString<AcceptorMetadataAccess<Self>>> {
&roots.sandbox_instance_id
}
}
impl StringAccess<Rc<Client>> for CommAccess {
fn with_string(data: &Rc<Client>, f: impl FnOnce(&str) -> bool) -> bool {
f(&data.pid_info.comm)
}
fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap<ClmMatchString<Self>> {
&roots.comm
}
}
impl StringAccess<Rc<Client>> for ExeAccess {
fn with_string(data: &Rc<Client>, f: impl FnOnce(&str) -> bool) -> bool {
f(&data.pid_info.exe)
}
fn nodes(roots: &RootMatchers) -> &ClmRootMatcherMap<ClmMatchString<Self>> {
&roots.exe
}
}

View file

@ -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<Rc<Client>> for ClmMatchUid {
fn matches(&self, data: &Rc<Client>) -> bool {
data.pid_info.uid == self.0
}
fn nodes(roots: &RootMatchers) -> Option<&RootMatcherMap<Rc<Client>, Self>> {
Some(&roots.uid)
}
}

View file

@ -0,0 +1,18 @@
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, CritUpstreamNodeBase, CritUpstreamNodeData,
},
};

View file

@ -0,0 +1,52 @@
use {
crate::criteria::{
CritMatcherId,
crit_graph::{CritTarget, crit_upstream::CritUpstreamNode},
},
std::rc::Rc,
};
pub struct CritDownstreamData<Target>
where
Target: CritTarget,
{
id: CritMatcherId,
pub(super) upstream: Vec<Rc<dyn CritUpstreamNode<Target>>>,
}
pub trait CritDownstream<Target>: 'static {
fn update_matched(self: Rc<Self>, target: &Target, matched: bool);
}
impl<Target> CritDownstreamData<Target>
where
Target: CritTarget,
{
pub fn new(id: CritMatcherId, upstream: &[Rc<dyn CritUpstreamNode<Target>>]) -> Self {
Self {
id,
upstream: upstream.to_vec(),
}
}
pub fn attach(&self, slf: &Rc<impl CritDownstream<Target>>) {
for upstream in &self.upstream {
upstream.attach(self.id, slf.clone() as _);
}
}
pub fn not(&self, mgr: &Target::Mgr) -> Vec<Rc<dyn CritUpstreamNode<Target>>> {
self.upstream.iter().map(|n| n.not(mgr)).collect()
}
}
impl<Target> Drop for CritDownstreamData<Target>
where
Target: CritTarget,
{
fn drop(&mut self) {
for el in &self.upstream {
el.detach(self.id);
}
}
}

View file

@ -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<Target, T>
where
Target: CritTarget,
T: CritMiddleCriterion<Target>,
{
upstream: CritDownstreamData<Target>,
downstream: CritUpstreamData<Target, CritMiddleData<T::Data>>,
criterion: T,
}
#[derive(Default)]
pub struct CritMiddleData<T> {
matches: usize,
data: T,
}
pub trait CritMiddleCriterion<Target>: Sized + 'static {
type Data: Default;
type Not: CritMiddleCriterion<Target>;
fn update_matched(&self, target: &Target, node: &mut Self::Data, matched: bool) -> bool;
fn pull(&self, upstream: &[Rc<dyn CritUpstreamNode<Target>>], target: &Target) -> bool;
fn not(&self) -> Self::Not;
}
impl<Target, T> CritMiddle<Target, T>
where
Target: CritTarget,
T: CritMiddleCriterion<Target>,
{
pub fn new(
mgr: &Target::Mgr,
upstream: &[Rc<dyn CritUpstreamNode<Target>>],
criterion: T,
) -> Rc<Self> {
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<Target, T> CritDownstream<Target> for CritMiddle<Target, T>
where
Target: CritTarget,
T: CritMiddleCriterion<Target>,
{
fn update_matched(self: Rc<Self>, 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<Target, T> CritUpstreamNodeBase<Target> for CritMiddle<Target, T>
where
Target: CritTarget,
T: CritMiddleCriterion<Target>,
{
type Data = CritMiddleData<T::Data>;
fn data(&self) -> &CritUpstreamData<Target, Self::Data> {
&self.downstream
}
fn not(&self, mgr: &Target::Mgr) -> Rc<dyn CritUpstreamNode<Target>> {
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<Target, T> CritDestroyListenerBase<Target> for CritMiddle<Target, T>
where
Target: CritTarget,
T: CritMiddleCriterion<Target>,
{
type Data = CritUpstreamNodeData<Target, CritMiddleData<T::Data>>;
fn data(&self) -> &CritPerTargetData<Target, Self::Data> {
&self.downstream.nodes
}
}

View file

@ -0,0 +1,171 @@
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<Target, T>
where
Target: CritTarget,
T: CritRootCriterion<Target>,
{
id: CritMatcherId,
downstream: CritUpstreamData<Target, ()>,
not: bool,
criterion: Rc<T>,
roots: Rc<Target::RootMatchers>,
}
pub struct CritRootFixed<Target, Crit>(pub Crit, pub PhantomData<fn(&Target)>);
pub trait CritRootCriterion<Target>: Sized + 'static
where
Target: CritTarget,
{
fn matches(&self, data: &Target) -> bool;
fn nodes(roots: &Target::RootMatchers) -> Option<&RootMatcherMap<Target, Self>> {
let _ = roots;
None
}
fn not(&self, mgr: &Target::Mgr) -> Option<Rc<dyn CritUpstreamNode<Target>>> {
let _ = mgr;
None
}
}
pub trait CritFixedRootCriterionBase<Target>: Sized + 'static
where
Target: CritTarget,
{
fn constant(&self) -> bool;
fn not<'a>(&self, mgr: &'a Target::Mgr) -> &'a FixedRootMatcher<Target, Self>
where
Self: CritFixedRootCriterion<Target>;
}
pub trait CritFixedRootCriterion<Target>: CritFixedRootCriterionBase<Target>
where
Target: CritTarget,
{
const COMPARE: bool = true;
fn matches(&self, data: &Target) -> bool;
}
impl<Target, T> CritRootCriterion<Target> for CritRootFixed<Target, T>
where
Target: CritTarget,
T: CritFixedRootCriterion<Target>,
{
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<Rc<dyn CritUpstreamNode<Target>>> {
Some(self.0.not(mgr)[!self.0.constant()].clone())
}
}
impl<Target, T> CritUpstreamNodeBase<Target> for CritRoot<Target, T>
where
Target: CritTarget,
T: CritRootCriterion<Target>,
{
type Data = ();
fn data(&self) -> &CritUpstreamData<Target, Self::Data> {
&self.downstream
}
fn not(&self, mgr: &Target::Mgr) -> Rc<dyn CritUpstreamNode<Target>> {
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<Target, T> CritRoot<Target, T>
where
Target: CritTarget,
T: CritRootCriterion<Target>,
{
pub fn new(roots: &Rc<Target::RootMatchers>, id: CritMatcherId, criterion: T) -> Rc<Self> {
Self::new_(roots, id, Rc::new(criterion), false)
}
fn new_(
roots: &Rc<Target::RootMatchers>,
id: CritMatcherId,
criterion: Rc<T>,
not: bool,
) -> Rc<Self> {
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
}
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);
}
pub fn has_downstream(&self) -> bool {
self.downstream.has_downstream()
}
}
impl<Target, T> CritDestroyListenerBase<Target> for CritRoot<Target, T>
where
Target: CritTarget,
T: CritRootCriterion<Target>,
{
type Data = CritUpstreamNodeData<Target, ()>;
fn data(&self) -> &CritPerTargetData<Target, Self::Data> {
&self.downstream.nodes
}
}
impl<Target, T> Drop for CritRoot<Target, T>
where
Target: CritTarget,
T: CritRootCriterion<Target>,
{
fn drop(&mut self) {
if let Some(nodes) = T::nodes(&self.roots) {
nodes.remove(&self.id);
}
}
}

View file

@ -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<Mgr = Self>;
fn id(&self) -> CritMatcherId;
fn leaf_events(&self) -> &Rc<AsyncQueue<CritLeafEvent<Self::Target>>>;
fn match_constant(&self) -> &FixedRootMatcher<Self::Target, CritMatchConstant<Self::Target>>;
fn roots(&self) -> &Rc<<Self::Target as CritTarget>::RootMatchers>;
}
pub trait CritTarget: 'static {
type Id: Copy + Hash + Eq;
type Mgr: CritMgr<Target = Self>;
type RootMatchers;
type LeafData: Copy + Eq;
type Owner: WeakCritTargetOwner<Target = Self>;
fn owner(&self) -> Self::Owner;
fn id(&self) -> Self::Id;
fn destroyed(&self) -> &CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<Self>>>;
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<Target = Self::Target>;
fn upgrade(&self) -> Option<Self::Owner>;
}

View file

@ -0,0 +1,175 @@
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<Target, T>
where
Target: CritTarget,
{
downstream: CopyHashMap<CritMatcherId, Weak<dyn CritDownstream<Target>>>,
pub nodes: CritPerTargetData<Target, CritUpstreamNodeData<Target, T>>,
}
pub struct CritUpstreamNodeData<Target, T>
where
Target: CritTarget,
{
matched: bool,
tl: Target::Owner,
data: T,
}
pub trait CritUpstreamNodeBase<Target>: 'static
where
Target: CritTarget,
{
type Data;
fn data(&self) -> &CritUpstreamData<Target, Self::Data>;
fn not(&self, mgr: &Target::Mgr) -> Rc<dyn CritUpstreamNode<Target>>;
fn pull(&self, target: &Target) -> bool;
}
pub trait CritUpstreamNode<Target>: 'static
where
Target: CritTarget,
{
fn attach(&self, id: CritMatcherId, downstream: Rc<dyn CritDownstream<Target>>);
fn detach(&self, id: CritMatcherId);
fn not(&self, mgr: &Target::Mgr) -> Rc<dyn CritUpstreamNode<Target>>;
fn pull(&self, target: &Target) -> bool;
fn get(&self, target: &Target) -> bool;
}
impl<Target, T> CritUpstreamNode<Target> for T
where
Target: CritTarget,
T: CritUpstreamNodeBase<Target>,
{
fn attach(&self, id: CritMatcherId, downstream: Rc<dyn CritDownstream<Target>>) {
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<dyn CritUpstreamNode<Target>> {
<T as CritUpstreamNodeBase<Target>>::not(self, mgr)
}
fn pull(&self, target: &Target) -> bool {
<T as CritUpstreamNodeBase<Target>>::pull(self, target)
}
fn get(&self, target: &Target) -> bool {
<T as CritUpstreamNodeBase<Target>>::data(self).matched(target)
}
}
impl<Target, T> Deref for CritUpstreamNodeData<Target, T>
where
Target: CritTarget,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<Target, T> DerefMut for CritUpstreamNodeData<Target, T>
where
Target: CritTarget,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}
impl<Target, T> CritUpstreamData<Target, T>
where
Target: CritTarget,
{
pub fn new(slf: &Weak<impl CritDestroyListener<Target>>, id: CritMatcherId) -> Self {
Self {
downstream: Default::default(),
nodes: CritPerTargetData::new(slf, id),
}
}
pub fn update_matched(
&self,
target: &Target,
mut node: RefMut<CritUpstreamNodeData<Target, T>>,
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<CritUpstreamNodeData<Target, T>>
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<RefMut<CritUpstreamNodeData<Target, T>>> {
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
}
}

155
src/criteria/crit_leaf.rs Normal file
View file

@ -0,0 +1,155 @@
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<Target>
where
Target: CritTarget,
{
upstream: CritDownstreamData<Target>,
on_match: Box<dyn Fn(Target::LeafData) -> Box<dyn FnOnce()>>,
targets: CritPerTargetData<Target, NodeHolder<Target>>,
events: Rc<AsyncQueue<CritLeafEvent<Target>>>,
}
pub(in crate::criteria) struct NodeHolder<Target>
where
Target: CritTarget,
{
node: Rc<Node<Target>>,
}
struct Node<Target>
where
Target: CritTarget,
{
leaf: Weak<CritLeafMatcher<Target>>,
target_id: Target::Id,
needs_event: Cell<bool>,
new_data: Cell<Option<Target::LeafData>>,
data: Cell<Option<Target::LeafData>>,
on_unmatch: Cell<Option<Box<dyn FnOnce()>>>,
}
pub struct CritLeafEvent<Target>
where
Target: CritTarget,
{
node: Rc<Node<Target>>,
}
impl<Target> CritDownstream<Target> for CritLeafMatcher<Target>
where
Target: CritTarget,
{
fn update_matched(self: Rc<Self>, 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<Target> CritLeafMatcher<Target>
where
Target: CritTarget,
{
pub(in crate::criteria) fn new(
mgr: &Target::Mgr,
upstream: &Rc<dyn CritUpstreamNode<Target>>,
on_match: impl Fn(Target::LeafData) -> Box<dyn FnOnce()> + 'static,
) -> Rc<Self> {
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<Node<Target>>, new_data: Option<Target::LeafData>) {
node.new_data.set(new_data);
if node.needs_event.replace(false) {
self.events.push(CritLeafEvent { node: node.clone() });
}
}
}
impl<Target> CritLeafEvent<Target>
where
Target: CritTarget,
{
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<Target> Drop for NodeHolder<Target>
where
Target: CritTarget,
{
fn drop(&mut self) {
if let Some(leaf) = self.node.leaf.upgrade() {
leaf.push_event(&self.node, None);
}
}
}
impl<Target> CritDestroyListenerBase<Target> for CritLeafMatcher<Target>
where
Target: CritTarget,
{
type Data = NodeHolder<Target>;
fn data(&self) -> &CritPerTargetData<Target, Self::Data> {
&self.targets
}
}

View file

@ -0,0 +1,4 @@
pub mod critm_any_or_all;
pub mod critm_constant;
pub mod critm_exactly;
pub mod critm_string;

View file

@ -0,0 +1,73 @@
use {
crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode},
std::{marker::PhantomData, rc::Rc},
};
pub struct CritMatchAnyOrAll<Target>
where
Target: CritTarget,
{
all: bool,
total: usize,
_phantom: PhantomData<fn(&Target)>,
}
impl<Target> CritMatchAnyOrAll<Target>
where
Target: CritTarget,
{
pub fn new(upstream: &[Rc<dyn CritUpstreamNode<Target>>], all: bool) -> Self {
Self {
all,
total: upstream.len(),
_phantom: Default::default(),
}
}
}
impl<Target> CritMiddleCriterion<Target> for CritMatchAnyOrAll<Target>
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<dyn CritUpstreamNode<Target>>], 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(),
}
}
}

View file

@ -0,0 +1,58 @@
use {
crate::criteria::{
CritMatcherIds, FixedRootMatcher,
crit_graph::{
CritFixedRootCriterion, CritFixedRootCriterionBase, CritMgr, CritRoot, CritRootFixed,
CritTarget,
},
},
linearize::static_map,
std::{marker::PhantomData, rc::Rc},
};
pub struct CritMatchConstant<Target>(pub bool, pub PhantomData<fn(&Target)>);
impl<Target> CritMatchConstant<Target>
where
Target: CritTarget,
{
pub fn create(
roots: &Rc<Target::RootMatchers>,
ids: &CritMatcherIds,
) -> FixedRootMatcher<Target, CritMatchConstant<Target>> {
static_map! {
v => CritRoot::new(
roots,
ids.next(),
CritRootFixed(Self(v, PhantomData), PhantomData),
),
}
}
}
impl<Target> CritFixedRootCriterionBase<Target> for CritMatchConstant<Target>
where
Target: CritTarget,
{
fn constant(&self) -> bool {
self.0
}
fn not<'a>(&self, mgr: &'a Target::Mgr) -> &'a FixedRootMatcher<Target, Self>
where
Self: CritFixedRootCriterion<Target>,
{
mgr.match_constant()
}
}
impl<Target> CritFixedRootCriterion<Target> for CritMatchConstant<Target>
where
Target: CritTarget,
{
const COMPARE: bool = false;
fn matches(&self, _data: &Target) -> bool {
self.0
}
}

View file

@ -0,0 +1,61 @@
use {
crate::criteria::crit_graph::{CritMiddleCriterion, CritTarget, CritUpstreamNode},
std::{marker::PhantomData, rc::Rc},
};
pub struct CritMatchExactly<Target> {
total: usize,
num: usize,
not: bool,
_phantom: PhantomData<fn(&Target)>,
}
impl<Target> CritMatchExactly<Target> {
pub fn new(upstream: &[Rc<dyn CritUpstreamNode<Target>>], num: usize) -> Self {
Self {
total: upstream.len(),
num,
not: false,
_phantom: Default::default(),
}
}
}
impl<Target> CritMiddleCriterion<Target> for CritMatchExactly<Target>
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<dyn CritUpstreamNode<Target>>], 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(),
}
}
}

View file

@ -0,0 +1,45 @@
use {
crate::criteria::{
CritLiteralOrRegex, RootMatcherMap,
crit_graph::{CritRootCriterion, CritTarget},
},
std::marker::PhantomData,
};
pub struct CritMatchString<Target, A> {
string: CritLiteralOrRegex,
_phantom: PhantomData<(fn(&Target), A)>,
}
pub trait StringAccess<Target>: Sized + 'static
where
Target: CritTarget,
{
fn with_string(data: &Target, f: impl FnOnce(&str) -> bool) -> bool;
fn nodes(
roots: &Target::RootMatchers,
) -> &RootMatcherMap<Target, CritMatchString<Target, Self>>;
}
impl<Target, A> CritMatchString<Target, A> {
pub fn new(string: CritLiteralOrRegex) -> Self {
Self {
string,
_phantom: Default::default(),
}
}
}
impl<Target, A> CritRootCriterion<Target> for CritMatchString<Target, A>
where
Target: CritTarget,
A: StringAccess<Target>,
{
fn matches(&self, data: &Target) -> bool {
A::with_string(data, |s| self.string.matches(s))
}
fn nodes(roots: &Target::RootMatchers) -> Option<&RootMatcherMap<Target, Self>> {
Some(A::nodes(roots))
}
}

View file

@ -0,0 +1,135 @@
use {
crate::criteria::{
CritMatcherId,
crit_graph::{CritTarget, CritTargetOwner, WeakCritTargetOwner},
},
ahash::AHashMap,
std::{
cell::{RefCell, RefMut},
ops::{Deref, DerefMut},
rc::Weak,
},
};
pub struct CritPerTargetData<Target, T>
where
Target: CritTarget,
{
id: CritMatcherId,
slf: Weak<dyn CritDestroyListener<Target>>,
data: RefCell<AHashMap<Target::Id, PerTreeNodeData<Target, T>>>,
}
pub struct PerTreeNodeData<Target, T>
where
Target: CritTarget,
{
node: Target::Owner,
data: T,
}
pub(super) trait CritDestroyListenerBase<Target>: 'static
where
Target: CritTarget,
{
type Data;
fn data(&self) -> &CritPerTargetData<Target, Self::Data>;
}
pub trait CritDestroyListener<Target>: 'static
where
Target: CritTarget,
{
fn destroyed(&self, target_id: Target::Id);
}
impl<Target, T> CritPerTargetData<Target, T>
where
Target: CritTarget,
{
pub fn new(slf: &Weak<impl CritDestroyListener<Target>>, 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<T> {
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<T>> {
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<Target::Id, PerTreeNodeData<Target, T>>> {
self.data.borrow_mut()
}
}
impl<Target, T> Drop for CritPerTargetData<Target, T>
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<Target, T> CritDestroyListener<Target> for T
where
Target: CritTarget,
T: CritDestroyListenerBase<Target>,
{
fn destroyed(&self, target_id: Target::Id) {
let _v = self.data().data.borrow_mut().remove(&target_id);
}
}
impl<Target, T> Deref for PerTreeNodeData<Target, T>
where
Target: CritTarget,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<Target, T> DerefMut for PerTreeNodeData<Target, T>
where
Target: CritTarget,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}

436
src/criteria/tlm.rs Normal file
View file

@ -0,0 +1,436 @@
pub mod tlm_matchers;
use {
crate::{
criteria::{
CritDestroyListener, CritLiteralOrRegex, CritMatcherId, CritMatcherIds, CritMgrExt,
CritUpstreamNode, FixedRootMatcher, RootMatcherMap,
clm::ClmUpstreamNode,
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_fullscreen::TlmMatchFullscreen,
tlmm_just_mapped::TlmMatchJustMapped,
tlmm_kind::TlmMatchKind,
tlmm_seat_focus::TlmMatchSeatFocus,
tlmm_string::{
TlmMatchAppId, TlmMatchClass, TlmMatchInstance, TlmMatchRole, TlmMatchTag,
TlmMatchTitle, TlmMatchWorkspace,
},
tlmm_urgent::TlmMatchUrgent,
tlmm_visible::TlmMatchVisible,
},
},
ifs::wl_seat::WlSeatGlobal,
state::State,
tree::{NodeId, ToplevelData, ToplevelNode},
utils::{
copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue,
toplevel_identifier::ToplevelIdentifier,
},
},
jay_config::window::WindowType,
linearize::static_map,
std::{
marker::PhantomData,
rc::{Rc, Weak},
},
};
bitflags! {
TlMatcherChange: u32;
TL_CHANGED_DESTROYED = 1 << 0,
TL_CHANGED_NEW = 1 << 1,
TL_CHANGED_TITLE = 1 << 2,
TL_CHANGED_APP_ID = 1 << 3,
TL_CHANGED_FLOATING = 1 << 4,
TL_CHANGED_VISIBLE = 1 << 5,
TL_CHANGED_URGENT = 1 << 6,
TL_CHANGED_SEAT_FOCI = 1 << 7,
TL_CHANGED_FULLSCREEN = 1 << 8,
TL_CHANGED_JUST_MAPPED = 1 << 9,
TL_CHANGED_TAG = 1 << 10,
TL_CHANGED_CLASS_INST = 1 << 11,
TL_CHANGED_ROLE = 1 << 12,
TL_CHANGED_WORKSPACE = 1 << 13,
}
type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>;
pub struct TlMatcherManager {
ids: Rc<CritMatcherIds>,
changes: AsyncQueue<Rc<dyn ToplevelNode>>,
leaf_events: Rc<AsyncQueue<CritLeafEvent<ToplevelData>>>,
handle_just_mapped: AsyncQueue<Rc<dyn ToplevelNode>>,
constant: TlmFixedRootMatcher<CritMatchConstant<ToplevelData>>,
floating: TlmFixedRootMatcher<TlmMatchFloating>,
visible: TlmFixedRootMatcher<TlmMatchVisible>,
urgent: TlmFixedRootMatcher<TlmMatchUrgent>,
fullscreen: TlmFixedRootMatcher<TlmMatchFullscreen>,
just_mapped: TlmFixedRootMatcher<TlmMatchJustMapped>,
matchers: Rc<RootMatchers>,
}
type TlmRootMatcherMap<T> = RootMatcherMap<ToplevelData, T>;
#[derive(Default)]
pub struct RootMatchers {
kinds: TlmRootMatcherMap<TlmMatchKind>,
clients: CopyHashMap<CritMatcherId, Weak<TlmMatchClient>>,
title: TlmRootMatcherMap<TlmMatchTitle>,
tag: TlmRootMatcherMap<TlmMatchTag>,
app_id: TlmRootMatcherMap<TlmMatchAppId>,
seat_foci: TlmRootMatcherMap<TlmMatchSeatFocus>,
class: TlmRootMatcherMap<TlmMatchClass>,
instance: TlmRootMatcherMap<TlmMatchInstance>,
role: TlmRootMatcherMap<TlmMatchRole>,
workspace: TlmRootMatcherMap<TlmMatchWorkspace>,
}
pub async fn handle_tl_changes(state: Rc<State>) {
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<State>) {
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 async fn handle_tl_just_mapped(state: Rc<State>) {
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<ToplevelData>;
pub type TlmLeafMatcher = CritLeafMatcher<ToplevelData>;
impl TlMatcherManager {
pub fn new(ids: &Rc<CritMatcherIds>) -> 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),
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<State>) {
for tl in state.toplevels.lock().values() {
if let Some(tl) = tl.upgrade() {
tl.tl_data().property_changed(TL_CHANGED_NEW);
}
}
}
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)
}
pub fn has_interest(&self, data: &ToplevelData, mut change: TlMatcherChange) -> bool {
if change.contains(TL_CHANGED_DESTROYED) && data.destroyed.is_not_empty() {
return true;
}
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);
unconditional!(clients);
if self.constant[true].has_downstream() {
return true;
}
change |= TlMatcherChange::all();
}
macro_rules! conditional {
($change:expr, $field:ident) => {
if change.contains($change) && self.matchers.$field.is_not_empty() {
return true;
}
};
}
macro_rules! fixed_conditional {
($change:expr, $field:ident) => {
if change.contains($change) {
fixed!($field);
}
};
}
conditional!(TL_CHANGED_TITLE, title);
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);
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);
fixed_conditional!(TL_CHANGED_FULLSCREEN, fullscreen);
fixed_conditional!(TL_CHANGED_JUST_MAPPED, just_mapped);
false
}
pub fn changed(&self, node: Rc<dyn ToplevelNode>) {
self.changes.push(node);
}
fn update_matches(&self, node: Rc<dyn ToplevelNode>) {
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())
};
}
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);
unconditional!(clients);
self.constant[true].handle(data);
}
macro_rules! conditional {
($change:expr, $field:ident) => {
if changed.contains($change) {
for m in handlers!($field) {
m.handle(data);
}
}
};
}
macro_rules! fixed_conditional {
($change:expr, $field:ident) => {
if changed.contains($change) {
fixed!($field);
}
};
}
conditional!(TL_CHANGED_TITLE, title);
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);
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);
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<TlmUpstreamNode> {
self.root(TlmMatchTitle::new(string))
}
pub fn app_id(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchAppId::new(string))
}
pub fn tag(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchTag::new(string))
}
pub fn floating(&self) -> Rc<TlmUpstreamNode> {
self.floating[true].clone()
}
pub fn kind(&self, kind: WindowType) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchKind::new(kind))
}
pub fn client(&self, state: &Rc<State>, client: &Rc<ClmUpstreamNode>) -> Rc<TlmUpstreamNode> {
TlmMatchClient::new(state, client)
}
pub fn visible(&self) -> Rc<TlmUpstreamNode> {
self.visible[true].clone()
}
pub fn fullscreen(&self) -> Rc<TlmUpstreamNode> {
self.fullscreen[true].clone()
}
pub fn urgent(&self) -> Rc<TlmUpstreamNode> {
self.urgent[true].clone()
}
pub fn just_mapped(&self) -> Rc<TlmUpstreamNode> {
self.just_mapped[true].clone()
}
pub fn seat_focus(&self, seat: &WlSeatGlobal) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchSeatFocus::new(seat.id()))
}
pub fn class(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchClass::new(string))
}
pub fn instance(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchInstance::new(string))
}
pub fn role(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchRole::new(string))
}
pub fn workspace(&self, string: CritLiteralOrRegex) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchWorkspace::new(string))
}
}
impl CritTarget for ToplevelData {
type Id = NodeId;
type Mgr = TlMatcherManager;
type RootMatchers = RootMatchers;
type LeafData = ToplevelIdentifier;
type Owner = Weak<dyn ToplevelNode>;
fn owner(&self) -> Self::Owner {
self.slf.clone()
}
fn id(&self) -> Self::Id {
self.node_id
}
fn destroyed(&self) -> &CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<Self>>> {
&self.destroyed
}
fn leaf_data(&self) -> Self::LeafData {
self.identifier.get()
}
}
impl CritTargetOwner for Rc<dyn ToplevelNode> {
type Target = ToplevelData;
fn data(&self) -> &Self::Target {
self.tl_data()
}
}
impl WeakCritTargetOwner for Weak<dyn ToplevelNode> {
type Target = ToplevelData;
type Owner = Rc<dyn ToplevelNode>;
fn upgrade(&self) -> Option<Self::Owner> {
self.upgrade()
}
}
impl CritMgr for TlMatcherManager {
type Target = ToplevelData;
fn id(&self) -> CritMatcherId {
self.ids.next()
}
fn leaf_events(&self) -> &Rc<AsyncQueue<CritLeafEvent<Self::Target>>> {
&self.leaf_events
}
fn match_constant(&self) -> &FixedRootMatcher<Self::Target, CritMatchConstant<Self::Target>> {
&self.constant
}
fn roots(&self) -> &Rc<<Self::Target as CritTarget>::RootMatchers> {
&self.matchers
}
}

View file

@ -0,0 +1,28 @@
macro_rules! fixed_root_criterion {
($ty:ty, $field:ident) => {
impl crate::criteria::crit_graph::CritFixedRootCriterionBase<crate::tree::ToplevelData>
for $ty
{
fn constant(&self) -> bool {
self.0
}
fn not<'a>(
&self,
mgr: &'a crate::criteria::tlm::TlMatcherManager,
) -> &'a crate::criteria::FixedRootMatcher<crate::tree::ToplevelData, Self> {
&mgr.$field
}
}
};
}
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;
pub mod tlmm_urgent;
pub mod tlmm_visible;

View file

@ -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<State>,
node: Rc<ClmUpstreamNode>,
upstream: CritDownstreamData<Rc<Client>>,
downstream: CritUpstreamData<ToplevelData, ()>,
}
impl TlmMatchClient {
pub fn new(state: &Rc<State>, node: &Rc<ClmUpstreamNode>) -> Rc<TlmMatchClient> {
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<ToplevelData> for TlmMatchClient {
type Data = ();
fn data(&self) -> &CritUpstreamData<ToplevelData, Self::Data> {
&self.downstream
}
fn not(&self, _mgr: &TlMatcherManager) -> Rc<dyn CritUpstreamNode<ToplevelData>> {
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<Rc<Client>> for TlmMatchClient {
fn update_matched(self: Rc<Self>, target: &Rc<Client>, 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<ToplevelData> for TlmMatchClient {
type Data = CritUpstreamNodeData<ToplevelData, ()>;
fn data(&self) -> &CritPerTargetData<ToplevelData, Self::Data> {
&self.downstream.nodes
}
}
impl Drop for TlmMatchClient {
fn drop(&mut self) {
self.state
.tl_matcher_manager
.matchers
.clients
.remove(&self.id);
}
}

View file

@ -0,0 +1,11 @@
use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData};
pub struct TlmMatchFloating(pub bool);
fixed_root_criterion!(TlmMatchFloating, floating);
impl CritFixedRootCriterion<ToplevelData> for TlmMatchFloating {
fn matches(&self, data: &ToplevelData) -> bool {
data.is_floating.get()
}
}

View file

@ -0,0 +1,11 @@
use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData};
pub struct TlmMatchFullscreen(pub bool);
fixed_root_criterion!(TlmMatchFullscreen, fullscreen);
impl CritFixedRootCriterion<ToplevelData> for TlmMatchFullscreen {
fn matches(&self, data: &ToplevelData) -> bool {
data.is_fullscreen.get()
}
}

View file

@ -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<ToplevelData> for TlmMatchJustMapped {
fn matches(&self, data: &ToplevelData) -> bool {
data.just_mapped()
}
}

View file

@ -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<ToplevelData> for TlmMatchKind {
fn matches(&self, data: &ToplevelData) -> bool {
self.kind.0.contains(data.kind.to_window_type().0)
}
fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap<Self>> {
Some(&roots.kinds)
}
}

View file

@ -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<ToplevelData> for TlmMatchSeatFocus {
fn matches(&self, data: &ToplevelData) -> bool {
data.seat_foci.contains(&self.id)
}
fn nodes(roots: &RootMatchers) -> Option<&TlmRootMatcherMap<Self>> {
Some(&roots.seat_foci)
}
}

View file

@ -0,0 +1,110 @@
use crate::{
criteria::{
crit_matchers::critm_string::{CritMatchString, StringAccess},
tlm::{RootMatchers, TlmRootMatcherMap},
},
tree::{ToplevelData, ToplevelType},
};
pub type TlmMatchString<T> = CritMatchString<ToplevelData, T>;
pub type TlmMatchTitle = TlmMatchString<TitleAccess>;
pub type TlmMatchAppId = TlmMatchString<AppIdAccess>;
pub type TlmMatchTag = TlmMatchString<TagAccess>;
pub type TlmMatchClass = TlmMatchString<ClassAccess>;
pub type TlmMatchInstance = TlmMatchString<InstanceAccess>;
pub type TlmMatchRole = TlmMatchString<RoleAccess>;
pub type TlmMatchWorkspace = TlmMatchString<WorkspaceAccess>;
pub struct TitleAccess;
pub struct AppIdAccess;
pub struct TagAccess;
pub struct ClassAccess;
pub struct InstanceAccess;
pub struct RoleAccess;
pub struct WorkspaceAccess;
impl StringAccess<ToplevelData> for TitleAccess {
fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool {
f(&data.title.borrow())
}
fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap<TlmMatchString<Self>> {
&roots.title
}
}
impl StringAccess<ToplevelData> for AppIdAccess {
fn with_string(data: &ToplevelData, f: impl FnOnce(&str) -> bool) -> bool {
f(&data.app_id.borrow())
}
fn nodes(roots: &RootMatchers) -> &TlmRootMatcherMap<TlmMatchString<Self>> {
&roots.app_id
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.tag
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.class
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.instance
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.role
}
}
impl StringAccess<ToplevelData> 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<TlmMatchString<Self>> {
&roots.workspace
}
}

View file

@ -0,0 +1,11 @@
use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData};
pub struct TlmMatchUrgent(pub bool);
fixed_root_criterion!(TlmMatchUrgent, urgent);
impl CritFixedRootCriterion<ToplevelData> for TlmMatchUrgent {
fn matches(&self, data: &ToplevelData) -> bool {
data.wants_attention.get()
}
}

View file

@ -0,0 +1,11 @@
use crate::{criteria::crit_graph::CritFixedRootCriterion, tree::ToplevelData};
pub struct TlmMatchVisible(pub bool);
fixed_root_criterion!(TlmMatchVisible, visible);
impl CritFixedRootCriterion<ToplevelData> for TlmMatchVisible {
fn matches(&self, data: &ToplevelData) -> bool {
data.visible.get()
}
}

View file

@ -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;
@ -30,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;

141
src/ifs/jay_client_query.rs Normal file
View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
ids: CopyHashMap<ClientId, ()>,
all: Cell<bool>,
}
impl JayClientQuery {
pub fn new(client: &Rc<Client>, 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<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
Ok(())
}
fn execute(&self, _req: Execute, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let handle_client = |client: &Rc<Client>| {
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<Self>) -> Result<(), Self::Error> {
self.all.set(true);
Ok(())
}
fn add_id(&self, req: AddId, _slf: &Rc<Self>) -> 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<ClientError>),
}
efrom!(JayClientQueryError, ClientError);

View file

@ -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,
@ -19,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,
},
@ -26,7 +28,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 +79,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
17
18
}
fn required_caps(&self) -> ClientCaps {
@ -223,7 +228,7 @@ impl JayCompositorRequestHandler for JayCompositor {
}
fn get_client_id(&self, _req: GetClientId, _slf: &Rc<Self>) -> 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 +372,6 @@ impl JayCompositorRequestHandler for JayCompositor {
}
fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc<Self>) -> 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 +379,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<Self>) -> Result<(), Self::Error> {
let seat = self.client.lookup(req.seat)?;
let obj = Rc::new(JaySelectWorkspace {
id: req.id,
client: self.client.clone(),
@ -393,7 +407,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 +492,29 @@ impl JayCompositorRequestHandler for JayCompositor {
self.client.add_client_obj(&obj)?;
Ok(())
}
fn create_client_query(
&self,
req: CreateClientQuery,
_slf: &Rc<Self>,
) -> 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<Self>) -> Result<(), Self::Error> {
self.client.state.clients.kill(ClientId::from_raw(req.id));
Ok(())
}
fn create_tree_query(&self, req: CreateTreeQuery, _slf: &Rc<Self>) -> 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! {

View file

@ -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();
}
}

View file

@ -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 })
}

458
src/ifs/jay_tree_query.rs Normal file
View file

@ -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<Client>,
pub tracker: Tracker<Self>,
pub version: Version,
recursive: Cell<bool>,
root: RefCell<Option<Root>>,
}
enum Root {
Display,
WorkspaceNode(Rc<Opt<WorkspaceNode>>),
WorkspaceName(String),
ToplevelId(ToplevelIdentifier),
}
impl JayTreeQuery {
pub fn new(client: &Rc<Client>, 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<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
Ok(())
}
fn execute(&self, _req: Execute, _slf: &Rc<Self>) -> 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<Self>) -> Result<(), Self::Error> {
*self.root.borrow_mut() = Some(Root::Display);
Ok(())
}
fn set_recursive(&self, req: SetRecursive, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.recursive.set(req.recursive != 0);
Ok(())
}
fn set_root_workspace(
&self,
req: SetRootWorkspace,
_slf: &Rc<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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<Self>,
) -> 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<ContainerNode>) {
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<XdgToplevel>) {
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<XdgPopup>) {
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<DisplayNode>) {
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<OutputNode>) {
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<FloatNode>) {
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<WorkspaceNode>) {
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<ZwlrLayerSurfaceV1>) {
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<Xwindow>) {
let s = self.0;
s.send_toplevel(node.tl_data());
s.send_end();
}
fn visit_placeholder(&mut self, node: &Rc<PlaceholderNode>) {
let s = self.0;
s.send_toplevel(node.tl_data());
s.send_end();
}
fn visit_lock_surface(&mut self, node: &Rc<ExtSessionLockSurfaceV1>) {
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<ClientError>),
#[error("Toplevel id is ill-formed")]
InvalidToplevelId(OpaqueError),
#[error("No root node was set")]
NoRootSet,
}
efrom!(JayTreeQueryError, ClientError);

View file

@ -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<dyn Node> {
self.keyboard_node.get()
}
pub fn get_keyboard_output(&self) -> Option<Rc<OutputNode>> {
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<Self>) {
@ -556,11 +530,7 @@ impl WlSeatGlobal {
pub fn kb_parent_container(&self) -> Option<Rc<ContainerNode>> {
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<Self>) {
@ -634,29 +590,7 @@ impl WlSeatGlobal {
Some(tl) => tl,
_ => return,
};
self.set_tl_floating(tl, floating);
}
pub fn set_tl_floating(self: &Rc<Self>, tl: Rc<dyn ToplevelNode>, 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) {

View file

@ -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<WlSeatGlobal>, node: Rc<dyn Node>, 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();

View file

@ -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,
@ -21,6 +21,7 @@ use {
xwayland::XWaylandEvent,
},
bstr::BString,
jay_config::window::TileState,
std::{
cell::{Cell, RefCell},
ops::{Deref, Not},
@ -90,10 +91,10 @@ pub struct XwindowInfo {
pub override_redirect: Cell<bool>,
pub extents: Cell<Rect>,
pub pending_extents: Cell<Rect>,
pub instance: RefCell<Option<BString>>,
pub class: RefCell<Option<BString>>,
pub instance: RefCell<Option<String>>,
pub class: RefCell<Option<String>>,
pub title: RefCell<Option<String>>,
pub role: RefCell<Option<BString>>,
pub role: RefCell<Option<String>>,
pub protocols: CopyHashMap<u32, ()>,
pub window_types: CopyHashMap<u32, ()>,
pub never_focus: Cell<bool>,
@ -205,16 +206,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(data.clone()),
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,
@ -235,6 +239,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 {
@ -256,6 +267,14 @@ impl Xwindow {
pub fn map_status_changed(self: &Rc<Self>) {
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 => {
@ -272,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

View file

@ -328,6 +328,12 @@ impl XdgSurface {
popup.popup.xdg.set_popup_stack(stack);
}
}
pub fn for_each_popup(&self, mut f: impl FnMut(&Rc<XdgPopup>)) {
for popup in self.popups.lock().values() {
f(&popup.popup);
}
}
}
impl XdgSurfaceRequestHandler for XdgSurface {

View file

@ -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 {

View file

@ -27,12 +27,14 @@ 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::*},
},
ahash::{AHashMap, AHashSet},
jay_config::window::TileState,
num_derive::FromPrimitive,
std::{
cell::{Cell, RefCell},
@ -91,6 +93,11 @@ pub enum Decoration {
Server,
}
#[derive(Debug)]
pub struct XdgToplevelToplevelData {
pub tag: RefCell<String>,
}
pub struct XdgToplevel {
pub id: XdgToplevelId,
pub state: Rc<State>,
@ -111,6 +118,7 @@ pub struct XdgToplevel {
is_mapped: Cell<bool>,
dialog: CloneCell<Option<Rc<XdgDialogV1>>>,
extents_set: Cell<bool>,
pub data: Rc<XdgToplevelToplevelData>,
}
impl Debug for XdgToplevel {
@ -133,11 +141,15 @@ impl XdgToplevel {
states.insert(STATE_CONSTRAINED_BOTTOM);
}
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(),
xdg: surface.clone(),
node_id: state.node_ids.next(),
node_id,
parent: Default::default(),
children: RefCell::new(Default::default()),
states: RefCell::new(states),
@ -152,12 +164,15 @@ impl XdgToplevel {
state,
String::new(),
Some(surface.surface.client.clone()),
ToplevelType::XdgToplevel(data.clone()),
node_id,
slf,
),
drag: Default::default(),
is_mapped: Cell::new(false),
dialog: Default::default(),
extents_set: Cell::new(false),
data,
}
}
@ -367,6 +382,31 @@ impl XdgToplevelRequestHandler for XdgToplevel {
}
impl XdgToplevel {
fn map(
self: &Rc<Self>,
parent: Option<&XdgToplevel>,
pos: Option<(&Rc<OutputNode>, 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<Self>, workspace: &Rc<WorkspaceNode>, abs_pos: Option<(i32, i32)>) {
let (width, height) = self.toplevel_data.float_size(workspace);
self.state
@ -460,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();

View file

@ -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<XdgPopup>)) {
for popup in self.popups.lock().values() {
f(&popup.popup);
}
}
}
impl ZwlrLayerSurfaceV1RequestHandler for ZwlrLayerSurfaceV1 {

View file

@ -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<Self>,
) -> 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(())
}

View file

@ -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,15 @@ impl IoUring {
pub fn cancel(&self, id: IoUringTaskId) {
self.ring.cancel_task(id);
}
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 +321,9 @@ struct IoUringData {
cached_accepts: Stack<Box<AcceptTask>>,
fd_ids_scratch: RefCell<Vec<c::c_int>>,
iteration: NumCell<u64>,
yields: SyncQueue<Waker>,
}
unsafe trait Task {
@ -326,6 +344,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 +358,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 {

32
src/io_uring/debounce.rs Normal file
View file

@ -0,0 +1,32 @@
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<u64>,
pub(super) max: u64,
pub(super) iteration: Cell<u64>,
pub(super) ring: Rc<IoUringData>,
}
impl Debouncer {
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);
}
}
}

View file

@ -124,6 +124,10 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
ServerMessage::InterestReady { .. } => {}
ServerMessage::Features { .. } => {}
ServerMessage::SwitchEvent { .. } => {}
ServerMessage::ClientMatcherMatched { .. } => {}
ServerMessage::ClientMatcherUnmatched { .. } => {}
ServerMessage::WindowMatcherMatched { .. } => {}
ServerMessage::WindowMatcherUnmatched { .. } => {}
}
}
@ -214,14 +218,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 +252,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 +274,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,
})

View file

@ -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))
@ -474,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)*)
}

View file

@ -59,6 +59,7 @@ mod cmm;
mod compositor;
mod config;
mod cpu_worker;
mod criteria;
mod cursor;
mod cursor_user;
mod damage;

View file

@ -24,9 +24,7 @@ linear_ids!(AcceptorIds, AcceptorId, u64);
struct Acceptor {
id: AcceptorId,
state: Rc<State>,
sandbox_engine: Option<String>,
app_id: Option<String>,
instance_id: Option<String>,
metadata: Rc<AcceptorMetadata>,
listen_fd: Rc<OwnedFd>,
close_fd: Rc<OwnedFd>,
caps: ClientCaps,
@ -34,6 +32,14 @@ struct Acceptor {
close_future: Cell<Option<SpawnedFuture<()>>>,
}
#[derive(Default)]
pub struct AcceptorMetadata {
pub sandboxed: bool,
pub sandbox_engine: Option<String>,
pub app_id: Option<String>,
pub instance_id: Option<String>,
}
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(""),
)
}
}

View file

@ -15,6 +15,7 @@ use {
compositor::LIBEI_SOCKET,
config::ConfigProxy,
cpu_worker::CpuWorker,
criteria::{clm::ClMatcherManager, tlm::TlMatcherManager},
cursor::{Cursor, ServerCursors},
cursor_user::{CursorUserGroup, CursorUserGroupId, CursorUserGroupIds, CursorUserIds},
damage::DamageVisualizer,
@ -56,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,
},
@ -80,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,
@ -110,6 +112,7 @@ use {
jay_config::{
PciId,
video::{GfxApi, Transform},
window::TileState,
},
std::{
cell::{Cell, RefCell},
@ -121,6 +124,7 @@ use {
time::Duration,
},
thiserror::Error,
uapi::OwnedFd,
};
pub struct State {
@ -240,6 +244,8 @@ pub struct State {
pub float_above_fullscreen: Cell<bool>,
pub icons: Icons,
pub show_pin_icon: Cell<bool>,
pub cl_matcher_manager: ClMatcherManager,
pub tl_matcher_manager: TlMatcherManager,
}
// impl Drop for State {
@ -261,11 +267,13 @@ pub struct ScreenlockState {
pub struct XWaylandState {
pub enabled: Cell<bool>,
pub pidfd: CloneCell<Option<Rc<OwnedFd>>>,
pub handler: RefCell<Option<SpawnedFuture<()>>>,
pub queue: Rc<AsyncQueue<XWaylandEvent>>,
pub ipc_device_ids: XIpcDeviceIds,
pub use_wire_scale: Cell<bool>,
pub wire_scale: Cell<Option<i32>>,
pub windows: CopyHashMap<XwindowId, Rc<Xwindow>>,
}
pub struct IdleState {
@ -655,23 +663,24 @@ impl State {
}
}
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
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);
}
}
}
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
let output = seat
pub fn ensure_map_workspace(&self, seat: Option<&Rc<WlSeatGlobal>>) -> Rc<WorkspaceNode> {
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();
let ws = output.ensure_workspace();
.unwrap()
.ensure_workspace()
}
pub fn map_tiled(self: &Rc<Self>, node: Rc<dyn ToplevelNode>) {
let seat = self.seat_queue.last();
self.do_map_tiled(seat.as_deref(), node.clone());
self.focus_after_map(node, seat.as_deref());
}
fn do_map_tiled(self: &Rc<Self>, seat: Option<&Rc<WlSeatGlobal>>, node: Rc<dyn ToplevelNode>) {
let ws = self.ensure_map_workspace(seat);
self.map_tiled_on(node, &ws);
}
@ -732,11 +741,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<dyn ToplevelNode>, seat: Option<&Rc<WlSeatGlobal>>) {
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<WlSeatGlobal>, name: &str) {
@ -947,6 +967,15 @@ impl State {
self.slow_ei_clients.clear();
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) {
self.toplevels.remove(&id);
if let Some(config) = self.config.get() {
config.toplevel_removed(id);
}
}
pub fn damage_hardware_cursors(&self, render: bool) {
@ -1361,6 +1390,10 @@ impl State {
};
ctx.supports_color_management()
}
pub fn initial_tile_state(&self, data: &ToplevelData) -> Option<TileState> {
self.config.get()?.initial_tile_state(data)
}
}
#[derive(Debug, Error)]

View file

@ -23,8 +23,10 @@ use {
},
wheel::{Wheel, WheelError},
wire::{
JayCompositor, JayCompositorId, JayDamageTracking, JayDamageTrackingId, WlCallbackId,
WlRegistryId, 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,
@ -63,8 +65,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 +197,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 +335,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 +362,88 @@ impl ToolClient {
self.jay_damage_tracking.set(Some(Some(id)));
Some(id)
}
pub async fn select_workspace(self: &Rc<Self>) -> 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<Self>) -> 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<Self>) -> 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 {

View file

@ -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());
@ -998,6 +1007,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;
@ -1140,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() {
@ -1236,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 {

View file

@ -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;
}
}

View file

@ -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<State>) {
impl PlaceholderNode {
pub fn new_for(state: &Rc<State>, node: Rc<dyn ToplevelNode>, slf: &Weak<Self>) -> 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(Some(node.tl_data().identifier.get())),
id,
slf,
),
destroyed: Default::default(),
@ -65,9 +68,17 @@ impl PlaceholderNode {
}
pub fn new_empty(state: &Rc<State>, slf: &Weak<Self>) -> 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(None),
id,
slf,
),
destroyed: Default::default(),
update_textures_scheduled: Default::default(),
state: state.clone(),

View file

@ -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 {

View file

@ -1,14 +1,25 @@
use {
crate::{
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritMatcherId,
tlm::{
TL_CHANGED_APP_ID, TL_CHANGED_DESTROYED, TL_CHANGED_FLOATING,
TL_CHANGED_FULLSCREEN, TL_CHANGED_NEW, TL_CHANGED_TITLE, TL_CHANGED_URGENT,
TL_CHANGED_VISIBLE, TL_CHANGED_WORKSPACE, TlMatcherChange,
},
},
ifs::{
ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1,
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_surface::WlSurface,
wl_seat::{NodeSeatState, SeatId, collect_kb_foci, collect_kb_foci2},
wl_surface::{
WlSurface, x_surface::xwindow::XwindowData,
xdg_surface::xdg_toplevel::XdgToplevelToplevelData,
},
},
rect::Rect,
state::State,
@ -30,6 +41,7 @@ use {
JayToplevelId,
},
},
jay_config::{window, window::WindowType},
std::{
cell::{Cell, RefCell},
ops::Deref,
@ -87,12 +99,22 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
.clone_from(&title);
data.placeholder.tl_title_changed();
}
data.property_changed(TL_CHANGED_TITLE);
}
fn tl_set_parent(&self, parent: Rc<dyn ContainingNode>) {
let data = self.tl_data();
data.parent.set(Some(parent.clone()));
data.is_floating.set(parent.node_is_float());
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();
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());
}
@ -109,6 +131,7 @@ impl<T: ToplevelNodeBase> 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 {
@ -254,7 +277,27 @@ impl ToplevelOpt {
}
}
pub enum ToplevelType {
Container,
Placeholder(Option<ToplevelIdentifier>),
XdgToplevel(Rc<XdgToplevelToplevelData>),
XWindow(Rc<XwindowData>),
}
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 {
pub node_id: NodeId,
pub kind: ToplevelType,
pub self_active: Cell<bool>,
pub client: Option<Rc<Client>>,
pub state: Rc<State>,
@ -269,6 +312,7 @@ pub struct ToplevelData {
pub workspace: CloneCell<Option<Rc<WorkspaceNode>>>,
pub title: RefCell<String>,
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
pub mapped_during_iteration: Cell<u64>,
pub pos: Cell<Rect>,
pub desired_extents: Cell<Rect>,
pub seat_state: NodeSeatState,
@ -284,6 +328,10 @@ pub struct ToplevelData {
pub ext_copy_sessions:
CopyHashMap<(ClientId, ExtImageCopyCaptureSessionV1Id), Rc<ExtImageCopyCaptureSessionV1>>,
pub slf: Weak<dyn ToplevelNode>,
pub destroyed: CopyHashMap<CritMatcherId, Weak<dyn CritDestroyListener<ToplevelData>>>,
pub changed_properties: Cell<TlMatcherChange>,
pub just_mapped_scheduled: Cell<bool>,
pub seat_foci: CopyHashMap<SeatId, ()>,
}
impl ToplevelData {
@ -291,11 +339,16 @@ impl ToplevelData {
state: &Rc<State>,
title: String,
client: Option<Rc<Client>>,
kind: ToplevelType,
node_id: impl Into<NodeId>,
slf: &Weak<T>,
) -> 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(),
@ -310,6 +363,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(),
@ -323,6 +377,10 @@ impl ToplevelData {
jay_screencasts: Default::default(),
ext_copy_sessions: Default::default(),
slf: slf.clone(),
destroyed: Default::default(),
changed_properties: Default::default(),
just_mapped_scheduled: Cell::new(false),
seat_foci: Default::default(),
}
}
@ -359,6 +417,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();
@ -372,7 +444,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());
}
{
@ -382,6 +454,7 @@ impl ToplevelData {
}
}
self.detach_node(node);
self.property_changed(TL_CHANGED_DESTROYED);
}
pub fn detach_node(&self, node: &dyn Node) {
@ -444,11 +517,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(
@ -510,6 +588,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()
@ -532,6 +611,7 @@ impl ToplevelData {
}
};
self.is_fullscreen.set(false);
self.property_changed(TL_CHANGED_FULLSCREEN);
match fd.workspace.fullscreen.get() {
None => {
log::error!(
@ -552,7 +632,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,13 +640,13 @@ 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) {
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();
@ -580,7 +660,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);
}
@ -593,12 +673,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<OutputNode> {
match self.output_opt() {
None => self.state.dummy_output.get().unwrap(),
@ -618,11 +704,15 @@ impl ToplevelData {
};
(0, 0)
}
pub fn just_mapped(&self) -> bool {
self.mapped_during_iteration.get() == self.state.eng.iteration()
}
}
impl Drop for ToplevelData {
fn drop(&mut self) {
self.state.toplevels.remove(&self.identifier.get());
self.state.remove_toplevel_id(self.identifier.get());
}
}
@ -664,3 +754,82 @@ pub fn default_tile_drag_bounds<T: ToplevelNodeBase + ?Sized>(t: &T, split: Cont
ContainerSplit::Vertical => t.node_absolute_position().height() / FACTOR,
}
}
pub fn toplevel_parent_container(tl: &dyn ToplevelNode) -> Option<Rc<ContainerNode>> {
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<State>, tl: Rc<dyn ToplevelNode>, 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<State>, tl: Rc<dyn ToplevelNode>, 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<State>, tl: Rc<dyn ToplevelNode>, ws: &Rc<WorkspaceNode>) {
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);
}
}

View file

@ -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;

View file

@ -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<A: UnsafeCellCloneSafe, B: UnsafeCellCloneSafe> UnsafeCellCloneSafe for (A, B) {}
unsafe impl UnsafeCellCloneSafe for Modifiers {}
unsafe impl UnsafeCellCloneSafe for NodeId {}
unsafe impl UnsafeCellCloneSafe for Window {}

View file

@ -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)> {

View file

@ -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<OwnedFd>, signal: c_int) -> Result<(), OsError> {
let res = unsafe {
syscall(
SYS_pidfd_send_signal,
pidfd.raw(),
signal,
ptr::null_mut::<siginfo_t>(),
0,
)
};
map_err!(res).map(drop).map_err(|e| e.into())
}

View file

@ -44,7 +44,6 @@ impl<T> SyncQueue<T> {
unsafe { self.el.get().deref_mut().is_empty() }
}
#[expect(dead_code)]
pub fn is_not_empty(&self) -> bool {
!self.is_empty()
}

View file

@ -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())
}

View file

@ -36,6 +36,10 @@ impl JayToplevelEventHandler for UsrJayToplevel {
Ok(())
}
fn client_id(&self, _ev: ClientId, _slf: &Rc<Self>) -> Result<(), Self::Error> {
Ok(())
}
fn done(&self, _ev: Done, slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.done(slf);

View file

@ -12,9 +12,12 @@ use {
wl_surface::x_surface::xwindow::{Xwindow, XwindowData},
},
io_uring::IoUringError,
security_context_acceptor::AcceptorMetadata,
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::{
@ -177,6 +180,7 @@ async fn run(
ClientCaps::all(),
ClientCaps::all(),
true,
&Rc::new(AcceptorMetadata::default()),
);
let client = match client {
Ok(c) => c,
@ -185,6 +189,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 {

View file

@ -4,6 +4,7 @@ use {
crate::{
async_engine::SpawnedFuture,
client::Client,
criteria::tlm::{TL_CHANGED_CLASS_INST, TL_CHANGED_ROLE},
ifs::{
ipc::{
DataOfferId, DataSourceId, DynDataOffer, DynDataSource, IpcLocation, IpcVtable,
@ -59,7 +60,7 @@ use {
xwayland::{XWaylandError, XWaylandEvent},
},
ahash::{AHashMap, AHashSet},
bstr::ByteSlice,
bstr::{ByteSlice, ByteVec},
futures_util::{FutureExt, select},
smallvec::SmallVec,
std::{
@ -1085,6 +1086,11 @@ impl Wm {
}
async fn load_window_wm_window_role(&self, data: &Rc<XwindowData>) {
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
@ -1100,6 +1106,7 @@ impl Wm {
}
Err(XconError::PropertyUnavailable) => {
data.info.role.borrow_mut().take();
property_changed();
return;
}
Err(e) => {
@ -1111,11 +1118,17 @@ 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<XwindowData>) {
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::<u8>(data.window_id, ATOM_WM_CLASS, 0, &mut buf)
@ -1130,6 +1143,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 +1152,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<XwindowData>, prop: u32, name: &str) {
@ -1459,6 +1475,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;

View file

@ -28,6 +28,7 @@ use {
status::MessageFormat,
theme::Color,
video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode},
window::{TileState, WindowType},
xwayland::XScalingMode,
},
std::{
@ -53,15 +54,20 @@ pub enum SimpleCommand {
ReloadConfigToml,
Split(Axis),
ToggleFloating,
SetFloating(bool),
ToggleFullscreen,
SetFullscreen(bool),
ToggleMono,
SetMono(bool),
ToggleSplit,
SetSplit(Axis),
Forward(bool),
EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool),
ToggleFloatAboveFullscreen,
SetFloatPinned(bool),
ToggleFloatPinned,
KillClient,
}
#[derive(Debug, Clone)]
@ -194,6 +200,85 @@ pub enum OutputMatch {
},
}
#[derive(Default, Debug, Clone)]
pub struct GenericMatch<Match> {
pub name: Option<String>,
pub not: Option<Box<Match>>,
pub all: Option<Vec<Match>>,
pub any: Option<Vec<Match>>,
pub exactly: Option<MatchExactly<Match>>,
}
#[derive(Debug, Clone)]
pub struct MatchExactly<Match> {
pub num: usize,
pub list: Vec<Match>,
}
#[derive(Debug, Clone)]
pub struct ClientRule {
pub name: Option<String>,
pub match_: ClientMatch,
pub action: Option<Action>,
pub latch: Option<Action>,
}
#[derive(Default, Debug, Clone)]
pub struct ClientMatch {
pub generic: GenericMatch<Self>,
pub sandbox_engine: Option<String>,
pub sandbox_engine_regex: Option<String>,
pub sandbox_app_id: Option<String>,
pub sandbox_app_id_regex: Option<String>,
pub sandbox_instance_id: Option<String>,
pub sandbox_instance_id_regex: Option<String>,
pub sandboxed: Option<bool>,
pub uid: Option<i32>,
pub pid: Option<i32>,
pub is_xwayland: Option<bool>,
pub comm: Option<String>,
pub comm_regex: Option<String>,
pub exe: Option<String>,
pub exe_regex: Option<String>,
}
#[derive(Debug, Clone)]
pub struct WindowRule {
pub name: Option<String>,
pub match_: WindowMatch,
pub action: Option<Action>,
pub latch: Option<Action>,
pub auto_focus: Option<bool>,
pub initial_tile_state: Option<TileState>,
}
#[derive(Default, Debug, Clone)]
pub struct WindowMatch {
pub generic: GenericMatch<Self>,
pub types: Option<WindowType>,
pub client: Option<ClientMatch>,
pub title: Option<String>,
pub title_regex: Option<String>,
pub app_id: Option<String>,
pub app_id_regex: Option<String>,
pub floating: Option<bool>,
pub visible: Option<bool>,
pub urgent: Option<bool>,
pub focused: Option<bool>,
pub fullscreen: Option<bool>,
pub just_mapped: Option<bool>,
pub tag: Option<String>,
pub tag_regex: Option<String>,
pub x_class: Option<String>,
pub x_class_regex: Option<String>,
pub x_instance: Option<String>,
pub x_instance_regex: Option<String>,
pub x_role: Option<String>,
pub x_role_regex: Option<String>,
pub workspace: Option<String>,
pub workspace_regex: Option<String>,
}
#[derive(Debug, Clone)]
pub enum DrmDeviceMatch {
Any(Vec<DrmDeviceMatch>),
@ -391,6 +476,8 @@ pub struct Config {
pub float: Option<Float>,
pub named_actions: Vec<NamedAction>,
pub max_action_depth: u64,
pub client_rules: Vec<ClientRule>,
pub window_rules: Vec<WindowRule>,
}
#[derive(Debug, Error)]

View file

@ -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;
@ -35,8 +37,12 @@ pub mod shortcuts;
mod status;
mod tearing;
mod theme;
mod tile_state;
mod ui_drag;
mod vrr;
mod window_match;
mod window_rule;
mod window_type;
mod xwayland;
#[derive(Debug, Error)]

View file

@ -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,
@ -124,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)

View file

@ -0,0 +1,160 @@
use {
crate::{
config::{
ClientMatch, GenericMatch, MatchExactly,
context::Context,
extractor::{Extractor, ExtractorError, arr, bol, n32, opt, s32, 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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
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,
uid,
pid,
is_xwayland,
comm,
comm_regex,
exe,
exe_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")),
opt(s32("uid")),
opt(s32("pid")),
opt(bol("is-xwayland")),
opt(str("comm")),
opt(str("comm-regex")),
opt(str("exe")),
opt(str("exe-regex")),
),
))?;
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,
},
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(),
uid: uid.despan(),
pid: pid.despan(),
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(),
})
}
}
pub struct ClientMatchExactlyParser<'a>(pub &'a Context<'a>);
impl Parser for ClientMatchExactlyParser<'_> {
type Value = MatchExactly<ClientMatch>;
type Error = ClientMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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,
})
}
}

View file

@ -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<String>, Spanned<Value>>,
) -> ParseResult<Self> {
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<ClientRule>;
type Error = ClientRuleParserError;
const EXPECTED: &'static [DataType] = &[DataType::Array];
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
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)
}
}

View file

@ -8,6 +8,7 @@ use {
parsers::{
action::ActionParser,
actions::ActionsParser,
client_rule::ClientRulesParser,
color_management::ColorManagementParser,
connector::ConnectorsParser,
drm_device::DrmDevicesParser,
@ -31,6 +32,7 @@ use {
theme::ThemeParser,
ui_drag::UiDragParser,
vrr::VrrParser,
window_rule::WindowRulesParser,
xwayland::XwaylandParser,
},
spanned::SpannedErrorExt,
@ -120,7 +122,14 @@ 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,
window_rules_val,
),
) = ext.extract((
(
opt(val("keymap")),
@ -163,6 +172,8 @@ impl Parser for ConfigParser<'_> {
opt(val("float")),
opt(val("actions")),
recover(opt(int("max-action-depth"))),
opt(val("clients")),
opt(val("windows")),
),
))?;
let mut keymap = None;
@ -419,6 +430,20 @@ 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)),
}
}
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,
@ -453,6 +478,8 @@ impl Parser for ConfigParser<'_> {
float,
named_actions,
max_action_depth,
client_rules,
window_rules,
})
}
}

View file

@ -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<Value>]) -> ParseResult<Self> {
let mut res = vec![];

View file

@ -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<Self> {
let ty = match string {
"tiled" => TileState::Tiled,
"floating" => TileState::Floating,
_ => {
return Err(TileStateParserError::UnknownTileState(string.to_owned()).spanned(span));
}
};
Ok(ty)
}
}

Some files were not shown because too many files have changed in this diff Show more