1
0
Fork 0
forked from wry/wry

config: add window-rule infrastructure

This commit is contained in:
Julian Orth 2025-05-01 17:49:21 +02:00
parent a6257910bb
commit 59f8acdfde
26 changed files with 1829 additions and 38 deletions

View file

@ -18,6 +18,7 @@ use {
criteria::{
CritMatcherIds,
clm::{ClMatcherManager, handle_cl_changes, handle_cl_leaf_events},
tlm::{TlMatcherManager, handle_tl_changes, handle_tl_leaf_events},
},
damage::{DamageVisualizer, visualize_damage},
dbus::Dbus,
@ -299,6 +300,7 @@ fn start_compositor2(
icons: Default::default(),
show_pin_icon: Cell::new(false),
cl_matcher_manager: ClMatcherManager::new(&crit_ids),
tl_matcher_manager: TlMatcherManager::new(&crit_ids),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);
@ -476,6 +478,11 @@ fn start_global_event_handlers(
"cl matcher leaf events",
handle_cl_leaf_events(state.clone()),
),
eng.spawn("tl matcher manager", handle_tl_changes(state.clone())),
eng.spawn(
"tl matcher leaf events",
handle_tl_leaf_events(state.clone()),
),
]
}

View file

@ -22,6 +22,7 @@ use {
input::{InputDevice, Seat, SwitchEvent},
keyboard::{mods::Modifiers, syms::KeySym},
video::{Connector, DrmDevice},
window,
},
libloading::Library,
std::{cell::Cell, io, mem, ptr, rc::Rc},
@ -218,6 +219,11 @@ impl ConfigProxy {
client_matchers: Default::default(),
client_matcher_cache: Default::default(),
client_matcher_leafs: Default::default(),
window_matcher_ids: NumCell::new(1),
window_matchers: Default::default(),
window_matcher_cache: Default::default(),
window_matcher_leafs: Default::default(),
window_matcher_std_kinds: state.tl_matcher_manager.kind(window::CLIENT_WINDOW),
});
let init_msg = bincode_ops()
.serialize(&InitMessage::V1(V1InitMessage {}))

View file

@ -10,7 +10,9 @@ use {
compositor::MAX_EXTENTS,
config::ConfigProxy,
criteria::{
CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode, clm::ClmLeafMatcher,
CritLiteralOrRegex, CritMgrExt, CritTarget, CritUpstreamNode,
clm::ClmLeafMatcher,
tlm::{TlmLeafMatcher, TlmUpstreamNode},
},
format::config_formats,
ifs::wl_seat::{SeatId, WlSeatGlobal},
@ -22,9 +24,9 @@ use {
theme::{Color, ThemeSized},
tree::{
ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode,
TearingMode, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig, move_ws_to_output,
toplevel_create_split, toplevel_parent_container, toplevel_set_floating,
toplevel_set_workspace,
TearingMode, ToplevelData, ToplevelNode, VrrMode, WorkspaceNode, WsMoveConfig,
move_ws_to_output, toplevel_create_split, toplevel_parent_container,
toplevel_set_floating, toplevel_set_workspace,
},
utils::{
asyncevent::AsyncEvent,
@ -42,7 +44,7 @@ use {
jay_config::{
_private::{
ClientCriterionIpc, ClientCriterionStringField, GenericCriterionIpc, PollableId,
WireMode, bincode_ops,
WindowCriterionIpc, WireMode, bincode_ops,
ipc::{ClientMessage, Response, ServerMessage, WorkspaceSource},
},
Axis, Direction, Workspace,
@ -64,7 +66,7 @@ use {
TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction,
Transform, VrrMode as ConfigVrrMode,
},
window::Window,
window::{Window, WindowMatcher},
xwayland::XScalingMode,
},
libloading::Library,
@ -115,6 +117,13 @@ pub(super) struct ConfigProxyHandler {
CopyHashMap<ClientMatcher, Rc<CachedCriterion<ClientCriterionIpc, Rc<Client>>>>,
pub client_matcher_cache: CriterionCache<ClientCriterionIpc, Rc<Client>>,
pub client_matcher_leafs: CopyHashMap<ClientMatcher, Rc<ClmLeafMatcher>>,
pub window_matcher_ids: NumCell<u64>,
pub window_matchers:
CopyHashMap<WindowMatcher, Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>>,
pub window_matcher_cache: CriterionCache<WindowCriterionIpc, ToplevelData>,
pub window_matcher_leafs: CopyHashMap<WindowMatcher, Rc<TlmLeafMatcher>>,
pub window_matcher_std_kinds: Rc<TlmUpstreamNode>,
}
pub struct Pollable {
@ -159,7 +168,6 @@ where
K: Hash + Eq,
T: CritTarget,
{
#[allow(clippy::allow_attributes, dead_code)]
fn any(&self, v: &impl Fn(&K) -> bool) -> bool {
v(&self.crit) || self.upstream.iter().any(|u| u.any(v))
}
@ -177,6 +185,9 @@ impl ConfigProxyHandler {
self.client_matcher_leafs.clear();
self.client_matchers.clear();
self.window_matcher_leafs.clear();
self.window_matchers.clear();
if let Some(path) = &self.path {
if let Err(e) = uapi::unlink(path.as_str()) {
log::error!("Could not unlink {}: {}", path, ErrorFmt(OsError(e.0)));
@ -1933,6 +1944,98 @@ impl ConfigProxyHandler {
Ok(())
}
fn get_window_matcher(
&self,
matcher: WindowMatcher,
) -> Result<Rc<CachedCriterion<WindowCriterionIpc, ToplevelData>>, CphError> {
self.window_matchers
.get(&matcher)
.ok_or(CphError::WindowMatcherDoesNotExist(matcher))
}
fn handle_create_window_matcher(
&self,
mut criterion: WindowCriterionIpc,
) -> Result<(), CphError> {
if let WindowCriterionIpc::Generic(generic) = &mut criterion {
self.sort_generic_matcher(generic, |m| m.0);
}
let id = WindowMatcher(self.window_matcher_ids.fetch_add(1));
let cache = &self.window_matcher_cache;
if let Some(matcher) = cache.get(&criterion) {
if let Some(matcher) = matcher.upgrade() {
self.window_matchers.set(id, matcher);
self.respond(Response::CreateWindowMatcher { matcher: id });
return Ok(());
}
}
let mgr = &self.state.tl_matcher_manager;
let mut upstream = vec![];
let matcher = match &criterion {
WindowCriterionIpc::Generic(m) => {
self.create_generic_matcher(mgr, m, &mut upstream, |m| self.get_window_matcher(*m))?
}
WindowCriterionIpc::String {
string,
field,
regex,
} => {
#[expect(unused_variables)]
let needle = match *regex {
true => {
let regex = Regex::new(string).map_err(CphError::InvalidRegex)?;
CritLiteralOrRegex::Regex(regex)
}
false => CritLiteralOrRegex::Literal(string.to_string()),
};
match *field {}
}
WindowCriterionIpc::Types(t) => mgr.kind(*t),
};
let cached = Rc::new(CachedCriterion {
crit: criterion.clone(),
cache: cache.clone(),
upstream,
node: matcher.clone(),
});
cache.set(criterion, Rc::downgrade(&cached));
self.window_matchers.set(id, cached);
self.respond(Response::CreateWindowMatcher { matcher: id });
Ok(())
}
fn handle_destroy_window_matcher(&self, matcher: WindowMatcher) {
self.window_matchers.remove(&matcher);
self.window_matcher_leafs.remove(&matcher);
}
fn handle_enable_window_matcher_events(
self: &Rc<Self>,
matcher: WindowMatcher,
) -> Result<(), CphError> {
if self.window_matcher_leafs.contains(&matcher) {
return Ok(());
}
let upstream = self.get_window_matcher(matcher)?;
let mut node = upstream.node.clone();
if !upstream.any(&|crit| matches!(crit, WindowCriterionIpc::Types(_))) {
let list = [self.window_matcher_std_kinds.clone(), node];
node = self.state.tl_matcher_manager.list(&list, true);
}
let slf = self.clone();
let leaf = self.state.tl_matcher_manager.leaf(&node, move |tl| {
let window = slf.tl_id_to_window(tl);
slf.send(&ServerMessage::WindowMatcherMatched { matcher, window });
let slf = slf.clone();
Box::new(move || {
slf.send(&ServerMessage::WindowMatcherUnmatched { matcher, window });
})
});
self.window_matcher_leafs.set(matcher, leaf);
self.state.tl_matcher_manager.rematch_all(&self.state);
Ok(())
}
fn spaces_change(&self) {
struct V;
impl NodeVisitorBase for V {
@ -2729,6 +2832,15 @@ impl ConfigProxyHandler {
ClientMessage::EnableClientMatcherEvents { matcher } => self
.handle_enable_client_matcher_events(matcher)
.wrn("enable_window_matcher_events")?,
ClientMessage::CreateWindowMatcher { criterion } => self
.handle_create_window_matcher(criterion)
.wrn("create_window_matcher")?,
ClientMessage::DestroyWindowMatcher { matcher } => {
self.handle_destroy_window_matcher(matcher)
}
ClientMessage::EnableWindowMatcherEvents { matcher } => self
.handle_enable_window_matcher_events(matcher)
.wrn("enable_window_matcher_events")?,
}
Ok(())
}
@ -2814,6 +2926,8 @@ enum CphError {
ClientMatcherDoesNotExist(ClientMatcher),
#[error("Could not parse regex")]
InvalidRegex(#[source] regex::Error),
#[error("Window matcher {0:?} does not exist")]
WindowMatcherDoesNotExist(WindowMatcher),
}
trait WithRequestName {

View file

@ -3,6 +3,7 @@ mod crit_graph;
pub mod crit_leaf;
mod crit_matchers;
mod crit_per_target_data;
pub mod tlm;
use {
crate::{

View file

@ -141,7 +141,6 @@ where
self.downstream.update_matched(target, node, new, !new);
}
#[expect(dead_code)]
pub fn has_downstream(&self) -> bool {
self.downstream.has_downstream()
}

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

@ -0,0 +1,271 @@
pub mod tlm_matchers;
use {
crate::{
criteria::{
CritDestroyListener, CritMatcherId, CritMatcherIds, CritMgrExt, CritUpstreamNode,
FixedRootMatcher, RootMatcherMap,
crit_graph::{CritMgr, CritTarget, CritTargetOwner, WeakCritTargetOwner},
crit_leaf::{CritLeafEvent, CritLeafMatcher},
crit_matchers::critm_constant::CritMatchConstant,
tlm::tlm_matchers::tlmm_kind::TlmMatchKind,
},
state::State,
tree::{NodeId, ToplevelData, ToplevelNode},
utils::{
copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, queue::AsyncQueue,
toplevel_identifier::ToplevelIdentifier,
},
},
jay_config::window::WindowType,
std::rc::{Rc, Weak},
};
bitflags! {
TlMatcherChange: u32;
TL_CHANGED_DESTROYED = 1 << 0,
TL_CHANGED_NEW = 1 << 1,
}
type TlmFixedRootMatcher<T> = FixedRootMatcher<ToplevelData, T>;
pub struct TlMatcherManager {
ids: Rc<CritMatcherIds>,
changes: AsyncQueue<Rc<dyn ToplevelNode>>,
leaf_events: Rc<AsyncQueue<CritLeafEvent<ToplevelData>>>,
constant: TlmFixedRootMatcher<CritMatchConstant<ToplevelData>>,
matchers: Rc<RootMatchers>,
}
type TlmRootMatcherMap<T> = RootMatcherMap<ToplevelData, T>;
#[derive(Default)]
pub struct RootMatchers {
kinds: TlmRootMatcherMap<TlmMatchKind>,
}
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 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());
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 tl in state.toplevels.lock().values() {
if let Some(tl) = tl.upgrade() {
tl.tl_data().property_changed(TL_CHANGED_NEW);
}
}
}
pub fn has_no_interest(&self, data: &ToplevelData, change: TlMatcherChange) -> bool {
!self.has_interest(data, change)
}
pub fn has_interest(&self, data: &ToplevelData, mut change: TlMatcherChange) -> bool {
if change.contains(TL_CHANGED_DESTROYED) && data.destroyed.is_not_empty() {
return true;
}
#[expect(unused_macros)]
macro_rules! fixed {
($name:ident) => {
if self.$name[false].has_downstream() || self.$name[true].has_downstream() {
return true;
}
};
}
if change.contains(TL_CHANGED_NEW) {
macro_rules! unconditional {
($field:ident) => {
if self.matchers.$field.is_not_empty() {
return true;
}
};
}
unconditional!(kinds);
if self.constant[true].has_downstream() {
return true;
}
change |= TlMatcherChange::all();
}
#[expect(unused_macros)]
macro_rules! conditional {
($change:expr, $field:ident) => {
if change.contains($change) && self.matchers.$field.is_not_empty() {
return true;
}
};
}
#[expect(unused_macros)]
macro_rules! fixed_conditional {
($change:expr, $field:ident) => {
if change.contains($change) {
fixed!($field);
}
};
}
false
}
pub fn changed(&self, node: Rc<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())
};
}
#[expect(unused_macros)]
macro_rules! fixed {
($name:ident) => {
self.$name[false].handle(data);
self.$name[true].handle(data);
};
}
if changed.contains(TL_CHANGED_NEW) {
changed |= TlMatcherChange::all();
macro_rules! unconditional {
($field:ident) => {
for m in handlers!($field) {
m.handle(data);
}
};
}
unconditional!(kinds);
self.constant[true].handle(data);
}
#[expect(unused_macros)]
macro_rules! conditional {
($change:expr, $field:ident) => {
if changed.contains($change) {
for m in handlers!($field) {
m.handle(data);
}
}
};
}
#[expect(unused_macros)]
macro_rules! fixed_conditional {
($change:expr, $field:ident) => {
if changed.contains($change) {
fixed!($field);
}
};
}
}
pub fn kind(&self, kind: WindowType) -> Rc<TlmUpstreamNode> {
self.root(TlmMatchKind::new(kind))
}
}
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,21 @@
#[expect(unused_macros)]
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_kind;

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

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

View file

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

View file

@ -1,6 +1,10 @@
use {
crate::{
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritMatcherId,
tlm::{TL_CHANGED_DESTROYED, TL_CHANGED_NEW, TlMatcherChange},
},
ifs::{
ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1,
@ -92,7 +96,10 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
fn tl_set_parent(&self, parent: Rc<dyn ContainingNode>) {
let data = self.tl_data();
data.parent.set(Some(parent.clone()));
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
if parent_was_none {
data.property_changed(TL_CHANGED_NEW);
}
data.is_floating.set(parent.node_is_float());
self.tl_set_workspace(&parent.cnode_workspace());
}
@ -275,7 +282,6 @@ impl ToplevelType {
}
pub struct ToplevelData {
#[expect(dead_code)]
pub node_id: NodeId,
pub kind: ToplevelType,
pub self_active: Cell<bool>,
@ -307,6 +313,8 @@ 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>,
}
impl ToplevelData {
@ -351,6 +359,8 @@ impl ToplevelData {
jay_screencasts: Default::default(),
ext_copy_sessions: Default::default(),
slf: slf.clone(),
destroyed: Default::default(),
changed_properties: Default::default(),
}
}
@ -387,6 +397,20 @@ impl ToplevelData {
(width, height)
}
pub fn property_changed(&self, change: TlMatcherChange) {
let mgr = &self.state.tl_matcher_manager;
let props = self.changed_properties.get();
if props.is_none() && mgr.has_no_interest(self, change) {
return;
}
self.changed_properties.set(props | change);
if props.is_none() && change.is_some() {
if let Some(node) = self.slf.upgrade() {
mgr.changed(node);
}
}
}
pub fn destroy_node(&self, node: &dyn Node) {
for jay_tl in self.jay_toplevels.lock().drain_values() {
jay_tl.destroy();
@ -410,6 +434,7 @@ impl ToplevelData {
}
}
self.detach_node(node);
self.property_changed(TL_CHANGED_DESTROYED);
}
pub fn detach_node(&self, node: &dyn Node) {