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

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