1
0
Fork 0
forked from wry/wry

config: add client-rule infrastructure

This commit is contained in:
Julian Orth 2025-05-04 18:02:17 +02:00
parent 17e715cde4
commit fd2163d658
32 changed files with 1804 additions and 27 deletions

View file

@ -4,7 +4,7 @@ mod logging;
pub(crate) mod string_error;
use {
crate::video::Mode,
crate::{client::ClientMatcher, video::Mode},
bincode::Options,
serde::{Deserialize, Serialize},
std::marker::PhantomData,
@ -64,3 +64,24 @@ impl WireMode {
pub struct PollableId(pub u64);
pub const DEFAULT_SEAT_NAME: &str = "default";
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum GenericCriterionIpc<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,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
pub enum ClientCriterionStringField {}

View file

@ -3,14 +3,15 @@
use {
crate::{
_private::{
Config, ConfigEntry, ConfigEntryGen, PollableId, VERSION, WireMode, bincode_ops,
ClientCriterionIpc, Config, ConfigEntry, ConfigEntryGen, GenericCriterionIpc,
PollableId, VERSION, WireMode, bincode_ops,
ipc::{
ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource,
},
logging,
},
Axis, Direction, ModifiedKeySym, PciId, Workspace,
client::Client,
client::{Client, ClientCriterion, ClientMatcher, MatchedClient},
exec::Command,
input::{
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile,
@ -112,10 +113,16 @@ pub(crate) struct ConfigClient {
status_task: Cell<Vec<JoinHandle<()>>>,
i3bar_separator: RefCell<Option<Rc<String>>>,
pressed_keysym: Cell<Option<KeySym>>,
client_match_handlers: RefCell<HashMap<ClientMatcher, ClientMatchHandler>>,
feat_mod_mask: Cell<bool>,
}
struct ClientMatchHandler {
cb: Callback<MatchedClient>,
latched: HashMap<Client, Box<dyn FnOnce()>>,
}
struct Interest {
result: Option<Result<(), String>>,
waker: Option<Waker>,
@ -245,6 +252,7 @@ pub unsafe extern "C" fn init(
status_task: Default::default(),
i3bar_separator: Default::default(),
pressed_keysym: Cell::new(None),
client_match_handlers: Default::default(),
feat_mod_mask: Cell::new(false),
});
let init = unsafe { slice::from_raw_parts(init, size) };
@ -280,6 +288,16 @@ macro_rules! get_response {
}
}
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[non_exhaustive]
enum GenericCriterion<'a, Crit, Matcher> {
Matcher(Matcher),
Not(&'a Crit),
All(&'a [Crit]),
Any(&'a [Crit]),
Exactly(usize, &'a [Crit]),
}
impl ConfigClient {
fn send(&self, msg: &ClientMessage) {
let mut buf = self.bufs.borrow_mut().pop().unwrap_or_default();
@ -1419,6 +1437,151 @@ impl ConfigClient {
self.send(&ClientMessage::ClientKill { client });
}
fn create_generic_matcher<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) {
#[expect(unused_macros)]
macro_rules! string {
($t:expr, $field:ident, $regex:expr) => {
ClientCriterionIpc::String {
string: $t.to_string(),
field: ClientCriterionStringField::$field,
regex: $regex,
}
};
}
let create_matcher = |criterion| {
let res = self.send_with_response(&ClientMessage::CreateClientMatcher {
criterion: ClientCriterionIpc::Generic(criterion),
});
get_response!(res, ClientMatcher(0), CreateClientMatcher { matcher });
matcher
};
let destroy_matcher = |matcher| {
self.send(&ClientMessage::DestroyClientMatcher { matcher });
};
let generic = |crit: GenericCriterion<ClientCriterion, ClientMatcher>| {
self.create_generic_matcher::<ClientCriterion, ClientMatcher>(
crit,
child,
|c| self.create_client_matcher_(c, true),
create_matcher,
destroy_matcher,
)
};
#[expect(unused_variables)]
let criterion = match criterion {
ClientCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)),
ClientCriterion::Not(c) => return generic(GenericCriterion::Not(c)),
ClientCriterion::All(c) => return generic(GenericCriterion::All(c)),
ClientCriterion::Any(c) => return generic(GenericCriterion::Any(c)),
ClientCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)),
};
#[expect(unreachable_code)]
let res = self.send_with_response(&ClientMessage::CreateClientMatcher { criterion });
get_response!(
res,
(ClientMatcher(0), false),
CreateClientMatcher { matcher }
);
(matcher, true)
}
pub fn set_client_matcher_handler(
&self,
matcher: ClientMatcher,
cb: impl FnMut(MatchedClient) + 'static,
) {
let cb = Rc::new(RefCell::new(cb));
let handlers = &mut *self.client_match_handlers.borrow_mut();
let handler = handlers.entry(matcher).or_insert_with(|| {
self.send(&ClientMessage::EnableClientMatcherEvents { matcher });
ClientMatchHandler {
cb: cb.clone(),
latched: Default::default(),
}
});
handler.cb = cb.clone();
}
pub fn set_client_matcher_latch_handler(
&self,
matcher: ClientMatcher,
client: Client,
cb: impl FnOnce() + 'static,
) {
let handlers = &mut *self.client_match_handlers.borrow_mut();
if let Some(handler) = handlers.get_mut(&matcher) {
handler.latched.insert(client, Box::new(cb));
}
}
pub fn destroy_client_matcher(&self, matcher: ClientMatcher) {
self.send(&ClientMessage::DestroyClientMatcher { matcher });
self.client_match_handlers.borrow_mut().remove(&matcher);
}
fn handle_msg(&self, msg: &[u8]) {
self.handle_msg2(msg);
self.dispatch_futures();
@ -1681,6 +1844,30 @@ impl ConfigClient {
run_cb("switch event", &cb, event);
}
}
ServerMessage::ClientMatcherMatched { matcher, client } => {
let cb = {
let handlers = self.client_match_handlers.borrow();
let Some(handler) = handlers.get(&matcher) else {
return;
};
handler.cb.clone()
};
let matched = MatchedClient { matcher, client };
cb.borrow_mut()(matched);
}
ServerMessage::ClientMatcherUnmatched { matcher, client } => {
let cb = {
let mut handlers = self.client_match_handlers.borrow_mut();
let Some(handler) = handlers.get_mut(&matcher) else {
return;
};
let Some(cb) = handler.latched.remove(&client) else {
return;
};
cb
};
cb();
}
}
}

View file

@ -1,8 +1,8 @@
use {
crate::{
_private::{PollableId, WireMode},
_private::{ClientCriterionIpc, PollableId, WireMode},
Axis, Direction, PciId, Workspace,
client::Client,
client::{Client, ClientMatcher},
input::{
FocusFollowsMouseMode, InputDevice, Seat, SwitchEvent, acceleration::AccelProfile,
capability::Capability,
@ -94,6 +94,14 @@ pub enum ServerMessage {
input_device: InputDevice,
event: SwitchEvent,
},
ClientMatcherMatched {
matcher: ClientMatcher,
client: Client,
},
ClientMatcherUnmatched {
matcher: ClientMatcher,
client: Client,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -664,6 +672,15 @@ pub enum ClientMessage<'a> {
window: Window,
pinned: bool,
},
CreateClientMatcher {
criterion: ClientCriterionIpc,
},
DestroyClientMatcher {
matcher: ClientMatcher,
},
EnableClientMatcherEvents {
matcher: ClientMatcher,
},
}
#[derive(Serialize, Deserialize, Debug)]
@ -884,6 +901,9 @@ pub enum Response {
GetWindowIsVisible {
visible: bool,
},
CreateClientMatcher {
matcher: ClientMatcher,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -1,6 +1,9 @@
//! Tools for inspecting and manipulating clients.
use serde::{Deserialize, Serialize};
use {
serde::{Deserialize, Serialize},
std::ops::Deref,
};
/// A client connected to the compositor.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
@ -34,3 +37,85 @@ impl Client {
pub fn clients() -> Vec<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>]),
}
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

@ -2,6 +2,10 @@ use {
crate::{
async_engine::SpawnedFuture,
client::{error::LookupError, objects::Objects},
criteria::{
CritDestroyListener, CritMatcherId,
clm::{CL_CHANGED_DESTROYED, CL_CHANGED_NEW, ClMatcherChange},
},
ifs::{
wl_display::WlDisplay,
wl_registry::WlRegistry,
@ -31,7 +35,7 @@ use {
fmt::{Debug, Display, Formatter},
mem,
ops::DerefMut,
rc::Rc,
rc::{Rc, Weak},
},
uapi::{OwnedFd, c},
};
@ -177,6 +181,8 @@ impl Clients {
)),
wire_scale: Default::default(),
focus_stealing_serial: Default::default(),
changed_properties: Default::default(),
destroyed: Default::default(),
});
track!(data, data);
let display = Rc::new(WlDisplay::new(&data));
@ -196,6 +202,7 @@ impl Clients {
data.pid_info.comm,
effective_caps,
);
client.data.property_changed(CL_CHANGED_NEW);
self.clients.borrow_mut().insert(client.data.id, client);
Ok(data)
}
@ -251,6 +258,7 @@ impl Drop for ClientHolder {
self.data.surfaces_by_xwayland_serial.clear();
self.data.remove_activation_tokens();
self.data.commit_timelines.clear();
self.data.property_changed(CL_CHANGED_DESTROYED);
if self.data.is_xwayland {
if let Some(pidfd) = self.data.state.xwayland.pidfd.get() {
if let Err(e) = pidfd_send_signal(&pidfd, c::SIGKILL) {
@ -296,6 +304,8 @@ pub struct Client {
pub commit_timelines: Rc<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 const NUM_CACHED_SERIAL_RANGES: usize = 64;
@ -501,6 +511,14 @@ impl Client {
self.state.activation_tokens.remove(token);
}
}
pub fn property_changed(self: &Rc<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,10 @@ use {
cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries},
config::ConfigProxy,
cpu_worker::{CpuWorker, CpuWorkerError},
criteria::{
CritMatcherIds,
clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events},
},
damage::{DamageVisualizer, visualize_damage},
dbus::Dbus,
ei::ei_client::EiClients,
@ -156,6 +160,7 @@ fn start_compositor2(
scales.add(Scale::from_int(1));
let cpu_worker = Rc::new(CpuWorker::new(&ring, &engine)?);
let color_manager = ColorManager::new();
let crit_ids = Rc::new(CritMatcherIds::default());
let state = Rc::new(State {
kb_ctx,
backend: CloneCell::new(Rc::new(DummyBackend)),
@ -293,6 +298,7 @@ fn start_compositor2(
float_above_fullscreen: Cell::new(false),
icons: Default::default(),
show_pin_icon: Cell::new(false),
cl_matcher_manager: ClMatcherManager::new(&crit_ids),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
@ -465,6 +471,11 @@ fn start_global_event_handlers(
"workspace manager done",
workspace_manager_done(state.clone()),
),
eng.spawn("cl matcher manager", handle_cl_changes(state.clone())),
eng.spawn(
"cl matcher leaf events",
handle_cl_leaf_events(state.clone()),
),
]
}

View file

@ -214,6 +214,10 @@ impl ConfigProxy {
window_ids: NumCell::new(1),
windows_from_tl_id: Default::default(),
windows_to_tl_id: Default::default(),
client_matcher_ids: NumCell::new(1),
client_matchers: Default::default(),
client_matcher_cache: Default::default(),
client_matcher_leafs: Default::default(),
});
let init_msg = bincode_ops()
.serialize(&InitMessage::V1(V1InitMessage {}))

View file

@ -9,6 +9,9 @@ use {
cmm::cmm_transfer_function::TransferFunction,
compositor::MAX_EXTENTS,
config::ConfigProxy,
criteria::{
CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, clm::ClmLeafMatcher,
},
format::config_formats,
ifs::wl_seat::{SeatId, WlSeatGlobal},
io_uring::TaskResultExt,
@ -38,11 +41,11 @@ use {
bincode::Options,
jay_config::{
_private::{
PollableId, WireMode, bincode_ops,
ClientCriterionIpc, GenericCriterionIpc, PollableId, WireMode, bincode_ops,
ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource},
},
Axis, Direction, Workspace,
client::Client as ConfigClient,
client::{Client as ConfigClient, ClientMatcher},
input::{
FocusFollowsMouseMode, InputDevice, Seat,
acceleration::{ACCEL_PROFILE_ADAPTIVE, ACCEL_PROFILE_FLAT, AccelProfile},
@ -65,7 +68,15 @@ use {
},
libloading::Library,
log::Level,
std::{cell::Cell, ops::Deref, rc::Rc, sync::Arc, time::Duration},
regex::Regex,
std::{
cell::Cell,
hash::Hash,
ops::Deref,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
},
thiserror::Error,
uapi::{OwnedFd, c, fcntl_dupfd_cloexec},
};
@ -97,6 +108,12 @@ pub(super) struct ConfigProxyHandler {
pub window_ids: NumCell<u64>,
pub windows_from_tl_id: CopyHashMap<ToplevelIdentifier, Window>,
pub windows_to_tl_id: CopyHashMap<Window, ToplevelIdentifier>,
pub client_matcher_ids: NumCell<u64>,
pub client_matchers:
CopyHashMap<ClientMatcher, Rc<CachedCriterion<ClientCriterionIpc, Rc<Client>>>>,
pub client_matcher_cache: CriterionCache<ClientCriterionIpc, Rc<Client>>,
pub client_matcher_leafs: CopyHashMap<ClientMatcher, Rc<ClmLeafMatcher>>,
}
pub struct Pollable {
@ -113,6 +130,40 @@ pub(super) struct TimerData {
_handler: SpawnedFuture<()>,
}
pub type CriterionCache<K, T> = Rc<CopyHashMap<K, Weak<CachedCriterion<K, T>>>>;
pub struct CachedCriterion<K, T>
where
K: Hash + Eq,
T: CritTarget,
{
crit: K,
cache: CriterionCache<K, T>,
upstream: Vec<Rc<CachedCriterion<K, T>>>,
node: Rc<dyn CritUpstreamNode<T>>,
}
impl<K, T> Drop for CachedCriterion<K, T>
where
K: Hash + Eq,
T: CritTarget,
{
fn drop(&mut self) {
self.cache.remove(&self.crit);
}
}
impl<K, T> CachedCriterion<K, T>
where
K: Hash + Eq,
T: CritTarget,
{
#[allow(clippy::allow_attributes, dead_code)]
fn any(&self, v: &impl Fn(&K) -> bool) -> bool {
v(&self.crit) || self.upstream.iter().any(|u| u.any(v))
}
}
impl ConfigProxyHandler {
pub fn do_drop(&self) {
self.dropped.set(true);
@ -122,6 +173,9 @@ impl ConfigProxyHandler {
self.pollables.clear();
self.client_matcher_leafs.clear();
self.client_matchers.clear();
if let Some(path) = &self.path {
if let Err(e) = uapi::unlink(path.as_str()) {
log::error!("Could not unlink {}: {}", path, ErrorFmt(OsError(e.0)));
@ -1725,6 +1779,148 @@ impl ConfigProxyHandler {
.ok_or(CphError::WindowDoesNotExist(window))
}
fn get_client_matcher(
&self,
matcher: ClientMatcher,
) -> Result<Rc<CachedCriterion<ClientCriterionIpc, Rc<Client>>>, CphError> {
self.client_matchers
.get(&matcher)
.ok_or(CphError::ClientMatcherDoesNotExist(matcher))
}
fn sort_generic_matcher<T, K>(
&self,
generic: &mut GenericCriterionIpc<T>,
key: impl FnMut(&T) -> K,
) where
K: Ord,
{
match generic {
GenericCriterionIpc::List { list, .. } | GenericCriterionIpc::Exactly { list, .. } => {
list.sort_by_key(key)
}
GenericCriterionIpc::Matcher(_) | GenericCriterionIpc::Not(_) => {}
}
}
fn create_generic_matcher<Crit, Matcher, Mgr>(
&self,
mgr: &Mgr,
generic: &GenericCriterionIpc<Matcher>,
upstream: &mut Vec<Rc<CachedCriterion<Crit, Mgr::Target>>>,
get_matcher: impl Fn(&Matcher) -> Result<Rc<CachedCriterion<Crit, Mgr::Target>>, CphError>,
) -> Result<Rc<dyn CritUpstreamNode<Mgr::Target>>, CphError>
where
Crit: Clone + Hash + Eq,
Mgr: CritMgrExt,
{
let mut get_upstream = |m: &Matcher| -> Result<_, CphError> {
let m = get_matcher(m)?;
let node = m.node.clone();
upstream.push(m);
Ok(node)
};
let node = match generic {
GenericCriterionIpc::Matcher(m) => get_matcher(m)?.node.clone(),
GenericCriterionIpc::Not(m) => mgr.not(&get_upstream(m)?),
GenericCriterionIpc::List { list, all } => {
let mut m = Vec::with_capacity(list.len());
for c in list {
m.push(get_upstream(c)?);
}
mgr.list(&m, *all)
}
GenericCriterionIpc::Exactly { list, num } => {
let mut m = Vec::with_capacity(list.len());
for c in list {
m.push(get_upstream(c)?);
}
mgr.exactly(&m, *num)
}
};
Ok(node)
}
fn handle_create_client_matcher(
&self,
mut criterion: ClientCriterionIpc,
) -> Result<(), CphError> {
if let ClientCriterionIpc::Generic(generic) = &mut criterion {
self.sort_generic_matcher(generic, |m| m.0);
}
let id = ClientMatcher(self.client_matcher_ids.fetch_add(1));
let cache = &self.client_matcher_cache;
if let Some(matcher) = cache.get(&criterion) {
if let Some(matcher) = matcher.upgrade() {
self.client_matchers.set(id, matcher);
self.respond(Response::CreateClientMatcher { matcher: id });
return Ok(());
}
}
let mgr = &self.state.cl_matcher_manager;
let mut upstream = vec![];
let matcher = match &criterion {
ClientCriterionIpc::Generic(m) => {
self.create_generic_matcher(mgr, m, &mut upstream, |m| self.get_client_matcher(*m))?
}
ClientCriterionIpc::String {
string,
field,
regex,
} => {
#[expect(unused_variables)]
let needle = match *regex {
true => {
let regex = Regex::new(string).map_err(CphError::InvalidRegex)?;
CritLiteralOrRegex::Regex(regex)
}
false => CritLiteralOrRegex::Literal(string.to_string()),
};
match *field {}
}
};
let cached = Rc::new(CachedCriterion {
crit: criterion.clone(),
cache: cache.clone(),
upstream,
node: matcher.clone(),
});
cache.set(criterion, Rc::downgrade(&cached));
self.client_matchers.set(id, cached);
self.respond(Response::CreateClientMatcher { matcher: id });
Ok(())
}
fn handle_destroy_client_matcher(&self, matcher: ClientMatcher) {
self.client_matchers.remove(&matcher);
self.client_matcher_leafs.remove(&matcher);
}
fn handle_enable_client_matcher_events(
self: &Rc<Self>,
matcher: ClientMatcher,
) -> Result<(), CphError> {
if self.client_matcher_leafs.contains(&matcher) {
return Ok(());
}
let upstream = self.get_client_matcher(matcher)?;
let slf = self.clone();
let leaf = self
.state
.cl_matcher_manager
.leaf(&upstream.node, move |id| {
let client = ConfigClient(id.raw());
slf.send(&ServerMessage::ClientMatcherMatched { matcher, client });
let slf = slf.clone();
Box::new(move || {
slf.send(&ServerMessage::ClientMatcherUnmatched { matcher, client });
})
});
self.client_matcher_leafs.set(matcher, leaf);
self.state.cl_matcher_manager.rematch_all(&self.state);
Ok(())
}
fn spaces_change(&self) {
struct V;
impl NodeVisitorBase for V {
@ -2512,6 +2708,15 @@ impl ConfigProxyHandler {
ClientMessage::GetWindowClient { window } => self
.handle_get_window_client(window)
.wrn("get_window_client")?,
ClientMessage::CreateClientMatcher { criterion } => self
.handle_create_client_matcher(criterion)
.wrn("create_window_matcher")?,
ClientMessage::DestroyClientMatcher { matcher } => {
self.handle_destroy_client_matcher(matcher)
}
ClientMessage::EnableClientMatcherEvents { matcher } => self
.handle_enable_client_matcher_events(matcher)
.wrn("enable_window_matcher_events")?,
}
Ok(())
}
@ -2593,6 +2798,10 @@ enum CphError {
WindowDoesNotExist(Window),
#[error("Window {0:?} is not visible")]
WindowNotVisible(Window),
#[error("Client matcher {0:?} does not exist")]
ClientMatcherDoesNotExist(ClientMatcher),
#[error("Could not parse regex")]
InvalidRegex(#[source] regex::Error),
}
trait WithRequestName {

View file

@ -1,3 +1,4 @@
pub mod clm;
mod crit_graph;
pub mod crit_leaf;
mod crit_matchers;
@ -27,7 +28,6 @@ type RootMatcherMap<Target, T> = CopyHashMap<CritMatcherId, Weak<CritRoot<Target
type FixedRootMatcher<Target, T> = StaticMap<bool, Rc<CritRoot<Target, CritRootFixed<Target, T>>>>;
#[derive(Clone)]
#[expect(dead_code)]
pub enum CritLiteralOrRegex {
Literal(String),
Regex(Regex),
@ -42,7 +42,6 @@ impl CritLiteralOrRegex {
}
}
#[expect(dead_code)]
pub trait CritMgrExt: CritMgr {
fn list(
&self,
@ -85,6 +84,7 @@ pub trait CritMgrExt: CritMgr {
upstream.not(self)
}
#[expect(dead_code)]
fn root<T>(&self, criterion: T) -> Rc<dyn CritUpstreamNode<Self::Target>>
where
T: CritRootCriterion<Self::Target>,

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

@ -0,0 +1,203 @@
pub mod clm_matchers;
use {
crate::{
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritMatcherId, CritMatcherIds, CritUpstreamNode, FixedRootMatcher,
RootMatcherMap,
crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner},
crit_leaf::{CritLeafEvent, CritLeafMatcher},
crit_matchers::critm_constant::CritMatchConstant,
},
state::State,
utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue},
},
std::rc::{Rc, Weak},
};
bitflags! {
ClMatcherChange: u32;
CL_CHANGED_DESTROYED = 1 << 0,
CL_CHANGED_NEW = 1 << 1,
}
type ClmFixedRootMatcher<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>>>,
matchers: Rc<RootMatchers>,
}
#[expect(dead_code)]
type ClmRootMatcherMap<T> = RootMatcherMap<Rc<Client>, T>;
#[derive(Default)]
pub struct RootMatchers {}
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;
}
}
#[expect(dead_code)]
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());
#[expect(unused_macros)]
macro_rules! bool {
($name:ident) => {{
static_map! {
v => CritRoot::new(
&matchers,
ids.next(),
CritRootFixed($name(v), PhantomData),
)
}
}};
}
Self {
constant: CritMatchConstant::create(&matchers, ids),
changes: Default::default(),
leaf_events: Default::default(),
ids: ids.clone(),
matchers,
}
}
pub fn clear(&self) {
self.changes.clear();
self.leaf_events.clear();
}
pub fn rematch_all(&self, state: &Rc<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;
}
#[expect(unused_macros)]
macro_rules! handlers {
($name:ident) => {
self.matchers
.$name
.lock()
.values()
.filter_map(|m| m.upgrade())
};
}
#[expect(unused_macros)]
macro_rules! fixed {
($name:ident) => {
self.$name[false].handle(data);
self.$name[true].handle(data);
};
}
if changed.contains(CL_CHANGED_NEW) {
changed |= ClMatcherChange::all();
#[expect(unused_macros)]
macro_rules! unconditional {
($field:ident) => {
for m in handlers!($field) {
m.handle(data);
}
};
}
self.constant[true].handle(data);
}
}
}
impl CritTarget for Rc<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,19 @@
#[expect(unused_macros)]
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
}
}
};
}

View file

@ -129,7 +129,6 @@ where
slf
}
#[expect(dead_code)]
pub fn handle(&self, target: &Target) {
let new = self.criterion.matches(target) ^ self.not;
let node = match new {

View file

@ -104,7 +104,6 @@ impl<Target> CritLeafEvent<Target>
where
Target: CritTarget,
{
#[expect(dead_code)]
pub fn run(self) {
let n = self.node;
n.needs_event.set(true);

View file

@ -16,7 +16,6 @@ impl<Target> CritMatchConstant<Target>
where
Target: CritTarget,
{
#[expect(dead_code)]
pub fn create(
roots: &Rc<Target::RootMatchers>,
ids: &CritMatcherIds,

View file

@ -41,7 +41,6 @@ pub trait CritDestroyListener<Target>: 'static
where
Target: CritTarget,
{
#[expect(dead_code)]
fn destroyed(&self, target_id: Target::Id);
}

View file

@ -266,7 +266,6 @@ impl IoUring {
self.ring.cancel_task(id);
}
#[expect(dead_code)]
pub fn debouncer(&self, max: u64) -> Debouncer {
Debouncer {
cur: Default::default(),

View file

@ -11,7 +11,6 @@ pub struct Debouncer {
}
impl Debouncer {
#[expect(dead_code)]
pub async fn debounce(&self) {
let iteration = self.ring.iteration.get();
if self.iteration.replace(iteration) != iteration {

View file

@ -124,6 +124,8 @@ unsafe extern "C" fn handle_msg(data: *const u8, msg: *const u8, size: usize) {
ServerMessage::InterestReady { .. } => {}
ServerMessage::Features { .. } => {}
ServerMessage::SwitchEvent { .. } => {}
ServerMessage::ClientMatcherMatched { .. } => {}
ServerMessage::ClientMatcherUnmatched { .. } => {}
}
}

View file

@ -475,6 +475,10 @@ macro_rules! bitflags {
self.0 != 0
}
pub fn is_none(self) -> bool {
self.0 == 0
}
pub fn all() -> Self {
Self(0 $(| $val)*)
}

View file

@ -15,6 +15,7 @@ use {
compositor::LIBEI_SOCKET,
config::ConfigProxy,
cpu_worker::CpuWorker,
criteria::clm::ClMatcherManager,
cursor::{Cursor, ServerCursors},
cursor_user::{CursorUserGroup, CursorUserGroupId, CursorUserGroupIds, CursorUserIds},
damage::DamageVisualizer,
@ -241,6 +242,7 @@ pub struct State {
pub float_above_fullscreen: Cell<bool>,
pub icons: Icons,
pub show_pin_icon: Cell<bool>,
pub cl_matcher_manager: ClMatcherManager,
}
// impl Drop for State {
@ -949,6 +951,7 @@ impl State {
self.slow_ei_clients.clear();
self.toplevels.clear();
self.workspace_managers.clear();
self.cl_matcher_manager.clear();
}
pub fn remove_toplevel_id(&self, id: ToplevelIdentifier) {

View file

@ -66,6 +66,7 @@ pub enum SimpleCommand {
ToggleFloatAboveFullscreen,
SetFloatPinned(bool),
ToggleFloatPinned,
KillClient,
}
#[derive(Debug, Clone)]
@ -198,6 +199,34 @@ pub enum OutputMatch {
},
}
#[derive(Default, Debug, Clone)]
pub struct GenericMatch<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>,
}
#[derive(Debug, Clone)]
pub enum DrmDeviceMatch {
Any(Vec<DrmDeviceMatch>),
@ -395,6 +424,7 @@ pub struct Config {
pub float: Option<Float>,
pub named_actions: Vec<NamedAction>,
pub max_action_depth: u64,
pub client_rules: Vec<ClientRule>,
}
#[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;

View file

@ -132,6 +132,7 @@ impl ActionParser<'_> {
"pin-float" => SetFloatPinned(true),
"unpin-float" => SetFloatPinned(false),
"toggle-float-pinned" => ToggleFloatPinned,
"kill-client" => KillClient,
_ => {
return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -0,0 +1,104 @@
use {
crate::{
config::{
ClientMatch, GenericMatch, MatchExactly,
context::Context,
extractor::{Extractor, ExtractorError, arr, n32, opt, str, val},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::{
toml_span::{DespanExt, Span, Spanned},
toml_value::Value,
},
},
indexmap::IndexMap,
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum ClientMatchParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
}
pub struct ClientMatchParser<'a>(pub &'a Context<'a>);
impl Parser for ClientMatchParser<'_> {
type Value = ClientMatch;
type Error = ClientMatchParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let ((name, not_val, all_val, any_val, exactly_val),) = ext.extract(((
opt(str("name")),
opt(val("not")),
opt(arr("all")),
opt(arr("any")),
opt(val("exactly")),
),))?;
let mut not = None;
if let Some(value) = not_val {
not = Some(Box::new(value.parse(&mut ClientMatchParser(self.0))?));
}
macro_rules! list {
($val:expr) => {{
let mut list = None;
if let Some(value) = $val {
let mut res = vec![];
for value in value.value {
res.push(value.parse(&mut ClientMatchParser(self.0))?);
}
list = Some(res);
}
list
}};
}
let all = list!(all_val);
let any = list!(any_val);
let mut exactly = None;
if let Some(value) = exactly_val {
exactly = Some(value.parse(&mut ClientMatchExactlyParser(self.0))?);
}
Ok(ClientMatch {
generic: GenericMatch {
name: name.despan_into(),
not,
all,
any,
exactly,
},
})
}
}
pub struct ClientMatchExactlyParser<'a>(pub &'a Context<'a>);
impl Parser for ClientMatchExactlyParser<'_> {
type Value = MatchExactly<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,
@ -120,7 +121,7 @@ impl Parser for ConfigParser<'_> {
ui_drag_val,
xwayland_val,
),
(color_management_val, float_val, actions_val, max_action_depth_val),
(color_management_val, float_val, actions_val, max_action_depth_val, client_rules_val),
) = ext.extract((
(
opt(val("keymap")),
@ -163,6 +164,7 @@ impl Parser for ConfigParser<'_> {
opt(val("float")),
opt(val("actions")),
recover(opt(int("max-action-depth"))),
opt(val("clients")),
),
))?;
let mut keymap = None;
@ -419,6 +421,13 @@ impl Parser for ConfigParser<'_> {
}
max_action_depth = value.value as _;
}
let mut client_rules = vec![];
if let Some(value) = client_rules_val {
match value.parse(&mut ClientRulesParser(self.0)) {
Ok(v) => client_rules = v,
Err(e) => log::warn!("Could not parse the client rules: {}", self.0.error(e)),
}
}
Ok(Config {
keymap,
repeat_rate,
@ -453,6 +462,7 @@ impl Parser for ConfigParser<'_> {
float,
named_actions,
max_action_depth,
client_rules,
})
}
}

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

@ -1,17 +1,27 @@
#![allow(clippy::len_zero, clippy::single_char_pattern, clippy::collapsible_if)]
#![allow(
clippy::len_zero,
clippy::single_char_pattern,
clippy::collapsible_if,
clippy::collapsible_else_if
)]
mod config;
mod rules;
mod toml;
use {
crate::config::{
Action, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch,
DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut, SimpleCommand,
Status, Theme, parse_config,
crate::{
config::{
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut,
SimpleCommand, Status, Theme, parse_config,
},
rules::{MatcherTemp, RuleMapper},
},
ahash::{AHashMap, AHashSet},
error_reporter::Report,
jay_config::{
client::Client,
config, config_dir,
exec::{Command, set_env, unset_env},
get_workspace,
@ -79,6 +89,16 @@ impl Action {
}
fn into_fn_impl<B: FnBuilder>(self, state: &Rc<State>) -> B {
macro_rules! client_action {
($name:ident, $opt:expr) => {{
let state = state.clone();
B::new(move || {
if let Some($name) = state.client.get() {
$opt
}
})
}};
}
let s = state.persistent.seat;
match self {
Action::SimpleCommand { cmd } => match cmd {
@ -115,6 +135,7 @@ impl Action {
SimpleCommand::ToggleFloatAboveFullscreen => B::new(toggle_float_above_fullscreen),
SimpleCommand::SetFloatPinned(pinned) => B::new(move || s.set_float_pinned(pinned)),
SimpleCommand::ToggleFloatPinned => B::new(move || s.toggle_float_pinned()),
SimpleCommand::KillClient => client_action!(c, c.kill()),
},
Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -666,6 +687,8 @@ struct State {
action_depth_max: u64,
action_depth: Cell<u64>,
client: Cell<Option<Client>>,
}
impl Drop for State {
@ -871,6 +894,16 @@ impl State {
}
}
}
fn with_client(&self, client: Client, check: bool, f: impl FnOnce()) {
let mut opt = Some(client);
if check && client.does_not_exist() {
opt = None;
}
self.client.set(opt);
f();
self.client.set(None);
}
}
#[derive(Eq, PartialEq, Hash)]
@ -887,6 +920,8 @@ struct PersistentState {
binds: RefCell<AHashSet<ModifiedKeySym>>,
#[expect(clippy::type_complexity)]
actions: RefCell<AHashMap<Rc<String>, Rc<dyn Fn()>>>,
client_rules: Cell<Vec<MatcherTemp<ClientRule>>>,
client_rule_mapper: RefCell<Option<RuleMapper<ClientRule>>>,
}
fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
@ -967,7 +1002,11 @@ fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
io_outputs: Default::default(),
action_depth_max: config.max_action_depth,
action_depth: Cell::new(0),
client: Default::default(),
});
let (client_rules, client_rule_mapper) = state.create_rules(&config.client_rules);
persistent.client_rules.set(client_rules);
*state.persistent.client_rule_mapper.borrow_mut() = Some(client_rule_mapper);
state.set_status(&config.status);
persistent.actions.borrow_mut().clear();
for a in config.named_actions {
@ -1190,10 +1229,15 @@ pub fn configure() {
seat: default_seat(),
binds: Default::default(),
actions: Default::default(),
client_rules: Default::default(),
client_rule_mapper: Default::default(),
});
{
let p = persistent.clone();
on_unload(move || p.actions.borrow_mut().clear());
on_unload(move || {
p.actions.borrow_mut().clear();
p.client_rule_mapper.borrow_mut().take();
});
}
load_config(true, &persistent);
}

258
toml-config/src/rules.rs Normal file
View file

@ -0,0 +1,258 @@
use {
crate::{
State,
config::{ClientMatch, ClientRule, GenericMatch},
},
ahash::{AHashMap, AHashSet},
jay_config::client::{ClientCriterion, ClientMatcher},
std::{mem::ManuallyDrop, rc::Rc},
};
impl State {
pub fn create_rules<R>(self: &Rc<Self>, rules: &[R]) -> (Vec<MatcherTemp<R>>, RuleMapper<R>)
where
R: Rule,
{
let mut names = AHashMap::new();
for (idx, rule) in rules.iter().enumerate() {
if let Some(name) = rule.name() {
names.insert(name.to_string(), idx);
}
}
let mut mapper = RuleMapper {
state: self.clone(),
names,
pending: Default::default(),
mapped: Default::default(),
};
let mut matchers = vec![];
for idx in 0..rules.len() {
if let Some(matcher) = mapper.map_rule(rules, idx) {
matchers.push(MatcherTemp(matcher));
}
}
(matchers, mapper)
}
}
pub trait Rule: Sized + 'static {
type Match;
type Matcher: Copy + 'static;
type Criterion<'a>;
const NAME_UPPER: &str;
const NAME_LOWER: &str;
fn name(&self) -> Option<&str>;
fn match_(&self) -> &Self::Match;
fn generic(m: &Self::Match) -> &GenericMatch<Self::Match>;
fn map_custom(
state: &Rc<State>,
all: &mut Vec<MatcherTemp<Self>>,
match_: &Self::Match,
) -> Option<()>;
fn create(c: Self::Criterion<'_>) -> Self::Matcher;
fn destroy(m: Self::Matcher);
fn bind(&self, state: &Rc<State>, matcher: Self::Matcher);
fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static>;
fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a>;
fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>;
fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>;
fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a>;
}
impl Rule for ClientRule {
type Match = ClientMatch;
type Matcher = ClientMatcher;
type Criterion<'a> = ClientCriterion<'a>;
const NAME_UPPER: &str = "Client";
const NAME_LOWER: &str = "client";
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
fn match_(&self) -> &Self::Match {
&self.match_
}
fn generic(m: &Self::Match) -> &GenericMatch<Self::Match> {
&m.generic
}
fn map_custom(
_state: &Rc<State>,
_all: &mut Vec<MatcherTemp<Self>>,
_match_: &Self::Match,
) -> Option<()> {
Some(())
}
fn create(c: Self::Criterion<'_>) -> Self::Matcher {
c.to_matcher()
}
fn destroy(m: Self::Matcher) {
m.destroy();
}
fn bind(&self, state: &Rc<State>, matcher: Self::Matcher) {
let state = state.clone();
macro_rules! latch {
($g:ident, $client:ident) => {
let g = $g.clone();
let state = state.clone();
$client.latch(move || {
state.with_client($client.client(), true, || g());
});
};
}
if let Some(action) = &self.action {
let f = action.clone().into_fn(&state);
if let Some(action) = &self.latch {
let g = action.clone().into_rc_fn(&state);
let state = state.clone();
matcher.bind(move |client| {
state.with_client(client.client(), false, &f);
latch!(g, client);
});
} else {
matcher.bind(move |client| {
state.with_client(client.client(), false, &f);
});
}
} else {
if let Some(action) = &self.latch {
let g = action.clone().into_rc_fn(&state);
matcher.bind(move |client| {
latch!(g, client);
});
}
}
}
fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> {
ClientCriterion::Matcher(m)
}
fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a> {
ClientCriterion::Not(m)
}
fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
ClientCriterion::All(m)
}
fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
ClientCriterion::Any(m)
}
fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
ClientCriterion::Exactly(n, m)
}
}
pub struct RuleMapper<R>
where
R: Rule,
{
state: Rc<State>,
names: AHashMap<String, usize>,
pending: AHashSet<usize>,
mapped: AHashMap<usize, R::Matcher>,
}
pub struct MatcherTemp<R>(R::Matcher)
where
R: Rule;
impl<R> Drop for MatcherTemp<R>
where
R: Rule,
{
fn drop(&mut self) {
R::destroy(self.0);
}
}
impl<R> RuleMapper<R>
where
R: Rule,
{
fn map_rule(&mut self, rules: &[R], idx: usize) -> Option<R::Matcher> {
if let Some(matcher) = self.mapped.get(&idx) {
return Some(*matcher);
}
if !self.pending.insert(idx) {
if let Some(name) = rules.get(idx).and_then(|r| r.name()) {
log::error!("{} rule `{name}` has a loop", R::NAME_UPPER);
}
return None;
}
let rule = &rules[idx];
let matcher = self.map_match(rules, rule.match_())?;
self.mapped.insert(idx, matcher);
rule.bind(&self.state, matcher);
Some(matcher)
}
fn map_temporary_match(&mut self, rules: &[R], matcher: &R::Match) -> Option<MatcherTemp<R>> {
self.map_match(rules, matcher).map(MatcherTemp)
}
fn map_match(&mut self, rules: &[R], matcher: &R::Match) -> Option<R::Matcher> {
let mut all = vec![];
self.map_generic_match(rules, &mut all, R::generic(matcher))?;
R::map_custom(&self.state, &mut all, matcher)?;
if all.len() == 1 {
return Some(ManuallyDrop::new(all.pop().unwrap()).0);
}
let all: Vec<_> = all.iter().map(|m| R::gen_matcher(m.0)).collect();
Some(R::create(R::gen_all(&all)))
}
fn map_generic_match(
&mut self,
rules: &[R],
all: &mut Vec<MatcherTemp<R>>,
matcher: &GenericMatch<R::Match>,
) -> Option<()> {
let m = |c: R::Criterion<'_>| MatcherTemp(R::create(c));
if let Some(name) = &matcher.name {
let Some(&idx) = self.names.get(&**name) else {
log::error!("There is no {} rule named `{name}`", R::NAME_LOWER);
return None;
};
let matcher = self.map_rule(rules, idx)?;
all.push(m(R::gen_matcher(matcher)));
}
if let Some(not) = &matcher.not {
let matcher = self.map_temporary_match(rules, not)?;
all.push(m(R::gen_not(&R::gen_matcher(matcher.0))));
}
if let Some(list) = &matcher.all {
for match_ in list {
all.push(self.map_temporary_match(rules, match_)?);
}
}
if let Some(list) = &matcher.any {
let mut any = vec![];
for match_ in list {
any.push(self.map_temporary_match(rules, match_)?);
}
let any: Vec<_> = any.iter().map(|m| R::gen_matcher(m.0)).collect();
all.push(m(R::gen_any(&any)));
}
if let Some(exactly) = &matcher.exactly {
let mut list = vec![];
for match_ in &exactly.list {
list.push(self.map_temporary_match(rules, match_)?);
}
let list: Vec<_> = list.iter().map(|m| R::gen_matcher(m.0)).collect();
all.push(m(R::gen_exactly(exactly.num, &list)))
}
Some(())
}
}

View file

@ -500,6 +500,86 @@
}
]
},
"ClientMatch": {
"description": "Criteria for matching clients.\n\nIf no fields are set, all clients are matched. If multiple fields are set, all fields\nmust match the client.\n",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Matches if the client rule with this name matches.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n\n # Matches the same clients as the previous rule.\n [[clients]]\n match.name = \"spotify\"\n ```\n"
},
"not": {
"description": "Matches if the contained criteria don't match.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"not-spotify\"\n match.not.sandbox-app-id = \"com.spotify.Client\"\n ```\n",
"$ref": "#/$defs/ClientMatch"
},
"all": {
"type": "array",
"description": "Matches if all of the contained criteria match.\n\n- Example:\n\n ```toml\n [[clients]]\n match.all = [\n { sandbox-app-id = \"com.spotify.Client\" },\n { sandbox-engine = \"org.flatpak\" },\n ]\n ```\n",
"items": {
"description": "",
"$ref": "#/$defs/ClientMatch"
}
},
"any": {
"type": "array",
"description": "Matches if any of the contained criteria match.\n\n- Example:\n\n ```toml\n [[clients]]\n match.any = [\n { sandbox-app-id = \"com.spotify.Client\" },\n { sandbox-app-id = \"com.valvesoftware.Steam\" },\n ]\n ```\n",
"items": {
"description": "",
"$ref": "#/$defs/ClientMatch"
}
},
"exactly": {
"description": "Matches if a specific number of contained criteria match.\n\n- Example:\n\n ```toml\n # Matches any client that is either steam or sandboxed by flatpak but not both.\n [[clients]]\n match.exactly.num = 1\n match.exactly.list = [\n { sandbox-engine = \"org.flatpak\" },\n { sandbox-app-id = \"com.valvesoftware.Steam\" },\n ]\n ```\n",
"$ref": "#/$defs/ClientMatchExactly"
}
},
"required": []
},
"ClientMatchExactly": {
"description": "Criterion for matching a specific number of client criteria.\n",
"type": "object",
"properties": {
"num": {
"type": "number",
"description": "The number of criteria that must match."
},
"list": {
"type": "array",
"description": "The list of criteria.",
"items": {
"description": "",
"$ref": "#/$defs/ClientMatch"
}
}
},
"required": [
"num",
"list"
]
},
"ClientRule": {
"description": "A client rule.\n",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of this rule.\n\nThis name can be referenced in other rules.\n\n- Example\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n\n [[clients]]\n match.name = \"spotify\"\n action = \"kill-client\"\n ```\n"
},
"match": {
"description": "The criteria that select the client that this rule applies to.",
"$ref": "#/$defs/ClientMatch"
},
"action": {
"description": "An action to execute when a client matches the criteria.",
"$ref": "#/$defs/Action"
},
"latch": {
"description": "An action to execute when a client no longer matches the criteria.",
"$ref": "#/$defs/Action"
}
},
"required": []
},
"Color": {
"type": "string",
"description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n"
@ -714,6 +794,14 @@
"type": "integer",
"description": "The maximum call depth of named actions. This setting prevents infinite recursion\nwhen using named actions. Setting this value to 0 or less disables named actions\ncompletely. The default is `16`.\n",
"minimum": 0.0
},
"clients": {
"type": "array",
"description": "An array of client rules.\n\nThese rules can be used to give names to clients and to manipulate them.\n\n- Example:\n\n ```toml\n [[clients]]\n name = \"spotify\"\n match.sandbox-app-id = \"com.spotify.Client\"\n ```\n",
"items": {
"description": "",
"$ref": "#/$defs/ClientRule"
}
}
},
"required": []
@ -1384,7 +1472,8 @@
"toggle-float-above-fullscreen",
"pin-float",
"unpin-float",
"toggle-float-pinned"
"toggle-float-pinned",
"kill-client"
]
},
"Status": {

View file

@ -700,6 +700,171 @@ The string should have one of the following values:
The brightness in cd/m^2.
<a name="types-ClientMatch"></a>
### `ClientMatch`
Criteria for matching clients.
If no fields are set, all clients are matched. If multiple fields are set, all fields
must match the client.
Values of this type should be tables.
The table has the following fields:
- `name` (optional):
Matches if the client rule with this name matches.
- Example:
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
# Matches the same clients as the previous rule.
[[clients]]
match.name = "spotify"
```
The value of this field should be a string.
- `not` (optional):
Matches if the contained criteria don't match.
- Example:
```toml
[[clients]]
name = "not-spotify"
match.not.sandbox-app-id = "com.spotify.Client"
```
The value of this field should be a [ClientMatch](#types-ClientMatch).
- `all` (optional):
Matches if all of the contained criteria match.
- Example:
```toml
[[clients]]
match.all = [
{ sandbox-app-id = "com.spotify.Client" },
{ sandbox-engine = "org.flatpak" },
]
```
The value of this field should be an array of [ClientMatchs](#types-ClientMatch).
- `any` (optional):
Matches if any of the contained criteria match.
- Example:
```toml
[[clients]]
match.any = [
{ sandbox-app-id = "com.spotify.Client" },
{ sandbox-app-id = "com.valvesoftware.Steam" },
]
```
The value of this field should be an array of [ClientMatchs](#types-ClientMatch).
- `exactly` (optional):
Matches if a specific number of contained criteria match.
- Example:
```toml
# Matches any client that is either steam or sandboxed by flatpak but not both.
[[clients]]
match.exactly.num = 1
match.exactly.list = [
{ sandbox-engine = "org.flatpak" },
{ sandbox-app-id = "com.valvesoftware.Steam" },
]
```
The value of this field should be a [ClientMatchExactly](#types-ClientMatchExactly).
<a name="types-ClientMatchExactly"></a>
### `ClientMatchExactly`
Criterion for matching a specific number of client criteria.
Values of this type should be tables.
The table has the following fields:
- `num` (required):
The number of criteria that must match.
The value of this field should be a number.
- `list` (required):
The list of criteria.
The value of this field should be an array of [ClientMatchs](#types-ClientMatch).
<a name="types-ClientRule"></a>
### `ClientRule`
A client rule.
Values of this type should be tables.
The table has the following fields:
- `name` (optional):
The name of this rule.
This name can be referenced in other rules.
- Example
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
[[clients]]
match.name = "spotify"
action = "kill-client"
```
The value of this field should be a string.
- `match` (optional):
The criteria that select the client that this rule applies to.
The value of this field should be a [ClientMatch](#types-ClientMatch).
- `action` (optional):
An action to execute when a client matches the criteria.
The value of this field should be a [Action](#types-Action).
- `latch` (optional):
An action to execute when a client no longer matches the criteria.
The value of this field should be a [Action](#types-Action).
<a name="types-Color"></a>
### `Color`
@ -1417,6 +1582,22 @@ The table has the following fields:
The numbers should be greater than or equal to 0.
- `clients` (optional):
An array of client rules.
These rules can be used to give names to clients and to manipulate them.
- Example:
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
```
The value of this field should be an array of [ClientRules](#types-ClientRule).
<a name="types-Connector"></a>
### `Connector`
@ -3129,6 +3310,12 @@ The string should have one of the following values:
Toggles whether the currently focused floating window is pinned.
- `kill-client`:
Kills a client.
This action has no effect outside of client rules.
<a name="types-Status"></a>

View file

@ -821,6 +821,11 @@ SimpleActionName:
- value: toggle-float-pinned
description: |
Toggles whether the currently focused floating window is pinned.
- value: kill-client
description: |
Kills a client.
This action has no effect outside of client rules.
Color:
@ -2487,6 +2492,23 @@ Config:
The maximum call depth of named actions. This setting prevents infinite recursion
when using named actions. Setting this value to 0 or less disables named actions
completely. The default is `16`.
clients:
kind: array
items:
ref: ClientRule
required: false
description: |
An array of client rules.
These rules can be used to give names to clients and to manipulate them.
- Example:
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
```
Idle:
@ -3016,3 +3038,149 @@ Float:
The default is `false`.
kind: boolean
required: false
ClientRule:
kind: table
description: |
A client rule.
fields:
name:
kind: string
required: false
description: |
The name of this rule.
This name can be referenced in other rules.
- Example
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
[[clients]]
match.name = "spotify"
action = "kill-client"
```
match:
ref: ClientMatch
required: false
description: The criteria that select the client that this rule applies to.
action:
ref: Action
required: false
description: An action to execute when a client matches the criteria.
latch:
ref: Action
required: false
description: An action to execute when a client no longer matches the criteria.
ClientMatch:
kind: table
description: |
Criteria for matching clients.
If no fields are set, all clients are matched. If multiple fields are set, all fields
must match the client.
fields:
name:
kind: string
required: false
description: |
Matches if the client rule with this name matches.
- Example:
```toml
[[clients]]
name = "spotify"
match.sandbox-app-id = "com.spotify.Client"
# Matches the same clients as the previous rule.
[[clients]]
match.name = "spotify"
```
not:
ref: ClientMatch
required: false
description: |
Matches if the contained criteria don't match.
- Example:
```toml
[[clients]]
name = "not-spotify"
match.not.sandbox-app-id = "com.spotify.Client"
```
all:
kind: array
items:
ref: ClientMatch
required: false
description: |
Matches if all of the contained criteria match.
- Example:
```toml
[[clients]]
match.all = [
{ sandbox-app-id = "com.spotify.Client" },
{ sandbox-engine = "org.flatpak" },
]
```
any:
kind: array
items:
ref: ClientMatch
required: false
description: |
Matches if any of the contained criteria match.
- Example:
```toml
[[clients]]
match.any = [
{ sandbox-app-id = "com.spotify.Client" },
{ sandbox-app-id = "com.valvesoftware.Steam" },
]
```
exactly:
ref: ClientMatchExactly
required: false
description: |
Matches if a specific number of contained criteria match.
- Example:
```toml
# Matches any client that is either steam or sandboxed by flatpak but not both.
[[clients]]
match.exactly.num = 1
match.exactly.list = [
{ sandbox-engine = "org.flatpak" },
{ sandbox-app-id = "com.valvesoftware.Steam" },
]
```
ClientMatchExactly:
kind: table
description: |
Criterion for matching a specific number of client criteria.
fields:
num:
kind: number
required: true
description: The number of criteria that must match.
list:
kind: array
items:
ref: ClientMatch
required: true
description: The list of criteria.