config: add client-rule infrastructure
This commit is contained in:
parent
17e715cde4
commit
fd2163d658
32 changed files with 1804 additions and 27 deletions
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
203
src/criteria/clm.rs
Normal 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
|
||||
}
|
||||
}
|
||||
19
src/criteria/clm/clm_matchers.rs
Normal file
19
src/criteria/clm/clm_matchers.rs
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ impl<Target> CritMatchConstant<Target>
|
|||
where
|
||||
Target: CritTarget,
|
||||
{
|
||||
#[expect(dead_code)]
|
||||
pub fn create(
|
||||
roots: &Rc<Target::RootMatchers>,
|
||||
ids: &CritMatcherIds,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ pub trait CritDestroyListener<Target>: 'static
|
|||
where
|
||||
Target: CritTarget,
|
||||
{
|
||||
#[expect(dead_code)]
|
||||
fn destroyed(&self, target_id: Target::Id);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)*)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
104
toml-config/src/config/parsers/client_match.rs
Normal file
104
toml-config/src/config/parsers/client_match.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
104
toml-config/src/config/parsers/client_rule.rs
Normal file
104
toml-config/src/config/parsers/client_rule.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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![];
|
||||
|
|
|
|||
|
|
@ -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
258
toml-config/src/rules.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue