config: add window-rule infrastructure
This commit is contained in:
parent
a6257910bb
commit
59f8acdfde
26 changed files with 1829 additions and 38 deletions
|
|
@ -4,7 +4,11 @@ mod logging;
|
|||
pub(crate) mod string_error;
|
||||
|
||||
use {
|
||||
crate::{client::ClientMatcher, video::Mode},
|
||||
crate::{
|
||||
client::ClientMatcher,
|
||||
video::Mode,
|
||||
window::{WindowMatcher, WindowType},
|
||||
},
|
||||
bincode::Options,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::marker::PhantomData,
|
||||
|
|
@ -95,3 +99,17 @@ pub enum ClientCriterionStringField {
|
|||
Comm,
|
||||
Exe,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum WindowCriterionIpc {
|
||||
Generic(GenericCriterionIpc<WindowMatcher>),
|
||||
String {
|
||||
string: String,
|
||||
field: WindowCriterionStringField,
|
||||
regex: bool,
|
||||
},
|
||||
Types(WindowType),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum WindowCriterionStringField {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use {
|
|||
crate::{
|
||||
_private::{
|
||||
ClientCriterionIpc, ClientCriterionStringField, Config, ConfigEntry, ConfigEntryGen,
|
||||
GenericCriterionIpc, PollableId, VERSION, WireMode, bincode_ops,
|
||||
GenericCriterionIpc, PollableId, VERSION, WindowCriterionIpc, WireMode, bincode_ops,
|
||||
ipc::{
|
||||
ClientMessage, InitMessage, Response, ServerFeature, ServerMessage, WorkspaceSource,
|
||||
},
|
||||
|
|
@ -31,7 +31,7 @@ use {
|
|||
Transform, VrrMode,
|
||||
connector_type::{CON_UNKNOWN, ConnectorType},
|
||||
},
|
||||
window::{Window, WindowType},
|
||||
window::{MatchedWindow, Window, WindowCriterion, WindowMatcher, WindowType},
|
||||
xwayland::XScalingMode,
|
||||
},
|
||||
bincode::Options,
|
||||
|
|
@ -114,6 +114,7 @@ pub(crate) struct ConfigClient {
|
|||
i3bar_separator: RefCell<Option<Rc<String>>>,
|
||||
pressed_keysym: Cell<Option<KeySym>>,
|
||||
client_match_handlers: RefCell<HashMap<ClientMatcher, ClientMatchHandler>>,
|
||||
window_match_handlers: RefCell<HashMap<WindowMatcher, WindowMatchHandler>>,
|
||||
|
||||
feat_mod_mask: Cell<bool>,
|
||||
}
|
||||
|
|
@ -123,6 +124,11 @@ struct ClientMatchHandler {
|
|||
latched: HashMap<Client, Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
struct WindowMatchHandler {
|
||||
cb: Callback<MatchedWindow>,
|
||||
latched: HashMap<Window, Box<dyn FnOnce()>>,
|
||||
}
|
||||
|
||||
struct Interest {
|
||||
result: Option<Result<(), String>>,
|
||||
waker: Option<Waker>,
|
||||
|
|
@ -253,6 +259,7 @@ pub unsafe extern "C" fn init(
|
|||
i3bar_separator: Default::default(),
|
||||
pressed_keysym: Cell::new(None),
|
||||
client_match_handlers: Default::default(),
|
||||
window_match_handlers: Default::default(),
|
||||
feat_mod_mask: Cell::new(false),
|
||||
});
|
||||
let init = unsafe { slice::from_raw_parts(init, size) };
|
||||
|
|
@ -1593,6 +1600,95 @@ impl ConfigClient {
|
|||
self.client_match_handlers.borrow_mut().remove(&matcher);
|
||||
}
|
||||
|
||||
pub fn create_window_matcher(&self, criterion: WindowCriterion) -> WindowMatcher {
|
||||
self.create_window_matcher_(criterion, false).0
|
||||
}
|
||||
|
||||
fn create_window_matcher_(
|
||||
&self,
|
||||
criterion: WindowCriterion,
|
||||
child: bool,
|
||||
) -> (WindowMatcher, bool) {
|
||||
#[expect(unused_macros)]
|
||||
macro_rules! string {
|
||||
($t:expr, $field:ident, $regex:expr) => {
|
||||
WindowCriterionIpc::String {
|
||||
string: $t.to_string(),
|
||||
field: WindowCriterionStringField::$field,
|
||||
regex: $regex,
|
||||
}
|
||||
};
|
||||
}
|
||||
let create_matcher = |criterion| {
|
||||
let res = self.send_with_response(&ClientMessage::CreateWindowMatcher {
|
||||
criterion: WindowCriterionIpc::Generic(criterion),
|
||||
});
|
||||
get_response!(res, WindowMatcher(0), CreateWindowMatcher { matcher });
|
||||
matcher
|
||||
};
|
||||
let destroy_matcher = |matcher| {
|
||||
self.send(&ClientMessage::DestroyWindowMatcher { matcher });
|
||||
};
|
||||
let generic = |crit: GenericCriterion<WindowCriterion, WindowMatcher>| {
|
||||
self.create_generic_matcher(
|
||||
crit,
|
||||
child,
|
||||
|c| self.create_window_matcher_(c, true),
|
||||
create_matcher,
|
||||
destroy_matcher,
|
||||
)
|
||||
};
|
||||
let criterion = match criterion {
|
||||
WindowCriterion::Matcher(m) => return generic(GenericCriterion::Matcher(m)),
|
||||
WindowCriterion::Not(c) => return generic(GenericCriterion::Not(c)),
|
||||
WindowCriterion::All(c) => return generic(GenericCriterion::All(c)),
|
||||
WindowCriterion::Any(c) => return generic(GenericCriterion::Any(c)),
|
||||
WindowCriterion::Exactly(n, c) => return generic(GenericCriterion::Exactly(n, c)),
|
||||
WindowCriterion::Types(t) => WindowCriterionIpc::Types(t),
|
||||
};
|
||||
let res = self.send_with_response(&ClientMessage::CreateWindowMatcher { criterion });
|
||||
get_response!(
|
||||
res,
|
||||
(WindowMatcher(0), false),
|
||||
CreateWindowMatcher { matcher }
|
||||
);
|
||||
(matcher, true)
|
||||
}
|
||||
|
||||
pub fn set_window_matcher_handler(
|
||||
&self,
|
||||
matcher: WindowMatcher,
|
||||
cb: impl FnMut(MatchedWindow) + 'static,
|
||||
) {
|
||||
let cb = Rc::new(RefCell::new(cb));
|
||||
let handlers = &mut *self.window_match_handlers.borrow_mut();
|
||||
let handler = handlers.entry(matcher).or_insert_with(|| {
|
||||
self.send(&ClientMessage::EnableWindowMatcherEvents { matcher });
|
||||
WindowMatchHandler {
|
||||
cb: cb.clone(),
|
||||
latched: Default::default(),
|
||||
}
|
||||
});
|
||||
handler.cb = cb.clone();
|
||||
}
|
||||
|
||||
pub fn set_window_matcher_latch_handler(
|
||||
&self,
|
||||
matcher: WindowMatcher,
|
||||
window: Window,
|
||||
cb: impl FnOnce() + 'static,
|
||||
) {
|
||||
let handlers = &mut *self.window_match_handlers.borrow_mut();
|
||||
if let Some(handler) = handlers.get_mut(&matcher) {
|
||||
handler.latched.insert(window, Box::new(cb));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destroy_window_matcher(&self, matcher: WindowMatcher) {
|
||||
self.send(&ClientMessage::DestroyWindowMatcher { matcher });
|
||||
self.window_match_handlers.borrow_mut().remove(&matcher);
|
||||
}
|
||||
|
||||
fn handle_msg(&self, msg: &[u8]) {
|
||||
self.handle_msg2(msg);
|
||||
self.dispatch_futures();
|
||||
|
|
@ -1879,6 +1975,30 @@ impl ConfigClient {
|
|||
};
|
||||
cb();
|
||||
}
|
||||
ServerMessage::WindowMatcherMatched { matcher, window } => {
|
||||
let cb = {
|
||||
let handlers = self.window_match_handlers.borrow();
|
||||
let Some(handler) = handlers.get(&matcher) else {
|
||||
return;
|
||||
};
|
||||
handler.cb.clone()
|
||||
};
|
||||
let matched = MatchedWindow { matcher, window };
|
||||
cb.borrow_mut()(matched);
|
||||
}
|
||||
ServerMessage::WindowMatcherUnmatched { matcher, window } => {
|
||||
let cb = {
|
||||
let mut handlers = self.window_match_handlers.borrow_mut();
|
||||
let Some(handler) = handlers.get_mut(&matcher) else {
|
||||
return;
|
||||
};
|
||||
let Some(cb) = handler.latched.remove(&window) else {
|
||||
return;
|
||||
};
|
||||
cb
|
||||
};
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use {
|
||||
crate::{
|
||||
_private::{ClientCriterionIpc, PollableId, WireMode},
|
||||
_private::{ClientCriterionIpc, PollableId, WindowCriterionIpc, WireMode},
|
||||
Axis, Direction, PciId, Workspace,
|
||||
client::{Client, ClientMatcher},
|
||||
input::{
|
||||
|
|
@ -15,7 +15,7 @@ use {
|
|||
ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction,
|
||||
Transform, VrrMode, connector_type::ConnectorType,
|
||||
},
|
||||
window::{Window, WindowType},
|
||||
window::{Window, WindowMatcher, WindowType},
|
||||
xwayland::XScalingMode,
|
||||
},
|
||||
serde::{Deserialize, Serialize},
|
||||
|
|
@ -102,6 +102,14 @@ pub enum ServerMessage {
|
|||
matcher: ClientMatcher,
|
||||
client: Client,
|
||||
},
|
||||
WindowMatcherMatched {
|
||||
matcher: WindowMatcher,
|
||||
window: Window,
|
||||
},
|
||||
WindowMatcherUnmatched {
|
||||
matcher: WindowMatcher,
|
||||
window: Window,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
@ -681,6 +689,15 @@ pub enum ClientMessage<'a> {
|
|||
EnableClientMatcherEvents {
|
||||
matcher: ClientMatcher,
|
||||
},
|
||||
CreateWindowMatcher {
|
||||
criterion: WindowCriterionIpc,
|
||||
},
|
||||
DestroyWindowMatcher {
|
||||
matcher: WindowMatcher,
|
||||
},
|
||||
EnableWindowMatcherEvents {
|
||||
matcher: WindowMatcher,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
@ -904,6 +921,9 @@ pub enum Response {
|
|||
CreateClientMatcher {
|
||||
matcher: ClientMatcher,
|
||||
},
|
||||
CreateWindowMatcher {
|
||||
matcher: WindowMatcher,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use {
|
||||
crate::{Axis, Direction, Workspace, client::Client},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::ops::Deref,
|
||||
};
|
||||
|
||||
/// A toplevel window.
|
||||
|
|
@ -202,3 +203,87 @@ impl Window {
|
|||
self.set_float_pinned(!self.float_pinned());
|
||||
}
|
||||
}
|
||||
|
||||
/// A window matcher.
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct WindowMatcher(pub u64);
|
||||
|
||||
/// A matched window.
|
||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct MatchedWindow {
|
||||
pub(crate) matcher: WindowMatcher,
|
||||
pub(crate) window: Window,
|
||||
}
|
||||
|
||||
/// A criterion for matching a window.
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum WindowCriterion<'a> {
|
||||
/// Matches if the contained matcher matches.
|
||||
Matcher(WindowMatcher),
|
||||
/// Matches if the contained criterion does not match.
|
||||
Not(&'a WindowCriterion<'a>),
|
||||
/// Matches if the window has one of the types.
|
||||
Types(WindowType),
|
||||
/// Matches if all of the contained criteria match.
|
||||
All(&'a [WindowCriterion<'a>]),
|
||||
/// Matches if any of the contained criteria match.
|
||||
Any(&'a [WindowCriterion<'a>]),
|
||||
/// Matches if an exact number of the contained criteria match.
|
||||
Exactly(usize, &'a [WindowCriterion<'a>]),
|
||||
}
|
||||
|
||||
impl WindowCriterion<'_> {
|
||||
/// Converts the criterion to a matcher.
|
||||
pub fn to_matcher(self) -> WindowMatcher {
|
||||
get!(WindowMatcher(0)).create_window_matcher(self)
|
||||
}
|
||||
|
||||
/// Binds a function to execute when the criterion matches a window.
|
||||
///
|
||||
/// This leaks the matcher.
|
||||
pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
|
||||
self.to_matcher().bind(cb);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMatcher {
|
||||
/// Destroys the matcher.
|
||||
///
|
||||
/// Any bound callback will no longer be executed.
|
||||
pub fn destroy(self) {
|
||||
get!().destroy_window_matcher(self);
|
||||
}
|
||||
|
||||
/// Sets a function to execute when the criterion matches a window.
|
||||
///
|
||||
/// Replaces any already bound callback.
|
||||
pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
|
||||
get!().set_window_matcher_handler(self, cb);
|
||||
}
|
||||
}
|
||||
|
||||
impl MatchedWindow {
|
||||
/// Returns the window that matched.
|
||||
pub fn window(self) -> Window {
|
||||
self.window
|
||||
}
|
||||
|
||||
/// Returns the matcher.
|
||||
pub fn matcher(self) -> WindowMatcher {
|
||||
self.matcher
|
||||
}
|
||||
|
||||
/// Latches a function to be executed when the window no longer matches the criteria.
|
||||
pub fn latch<F: FnOnce() + 'static>(self, cb: F) {
|
||||
get!().set_window_matcher_latch_handler(self.matcher, self.window, cb);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MatchedWindow {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.window
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod crit_graph;
|
|||
pub mod crit_leaf;
|
||||
mod crit_matchers;
|
||||
mod crit_per_target_data;
|
||||
pub mod tlm;
|
||||
|
||||
use {
|
||||
crate::{
|
||||
|
|
|
|||
|
|
@ -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
271
src/criteria/tlm.rs
Normal 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
|
||||
}
|
||||
}
|
||||
21
src/criteria/tlm/tlm_matchers.rs
Normal file
21
src/criteria/tlm/tlm_matchers.rs
Normal 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;
|
||||
31
src/criteria/tlm/tlm_matchers/tlmm_kind.rs
Normal file
31
src/criteria/tlm/tlm_matchers/tlmm_kind.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ use {
|
|||
status::MessageFormat,
|
||||
theme::Color,
|
||||
video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode},
|
||||
window::WindowType,
|
||||
xwayland::XScalingMode,
|
||||
},
|
||||
std::{
|
||||
|
|
@ -241,6 +242,20 @@ pub struct ClientMatch {
|
|||
pub exe_regex: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WindowRule {
|
||||
pub name: Option<String>,
|
||||
pub match_: WindowMatch,
|
||||
pub action: Option<Action>,
|
||||
pub latch: Option<Action>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct WindowMatch {
|
||||
pub generic: GenericMatch<Self>,
|
||||
pub types: Option<WindowType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DrmDeviceMatch {
|
||||
Any(Vec<DrmDeviceMatch>),
|
||||
|
|
@ -439,6 +454,7 @@ pub struct Config {
|
|||
pub named_actions: Vec<NamedAction>,
|
||||
pub max_action_depth: u64,
|
||||
pub client_rules: Vec<ClientRule>,
|
||||
pub window_rules: Vec<WindowRule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ mod tearing;
|
|||
mod theme;
|
||||
mod ui_drag;
|
||||
mod vrr;
|
||||
mod window_match;
|
||||
mod window_rule;
|
||||
mod window_type;
|
||||
mod xwayland;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ use {
|
|||
theme::ThemeParser,
|
||||
ui_drag::UiDragParser,
|
||||
vrr::VrrParser,
|
||||
window_rule::WindowRulesParser,
|
||||
xwayland::XwaylandParser,
|
||||
},
|
||||
spanned::SpannedErrorExt,
|
||||
|
|
@ -121,7 +122,14 @@ impl Parser for ConfigParser<'_> {
|
|||
ui_drag_val,
|
||||
xwayland_val,
|
||||
),
|
||||
(color_management_val, float_val, actions_val, max_action_depth_val, client_rules_val),
|
||||
(
|
||||
color_management_val,
|
||||
float_val,
|
||||
actions_val,
|
||||
max_action_depth_val,
|
||||
client_rules_val,
|
||||
window_rules_val,
|
||||
),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("keymap")),
|
||||
|
|
@ -165,6 +173,7 @@ impl Parser for ConfigParser<'_> {
|
|||
opt(val("actions")),
|
||||
recover(opt(int("max-action-depth"))),
|
||||
opt(val("clients")),
|
||||
opt(val("windows")),
|
||||
),
|
||||
))?;
|
||||
let mut keymap = None;
|
||||
|
|
@ -428,6 +437,13 @@ impl Parser for ConfigParser<'_> {
|
|||
Err(e) => log::warn!("Could not parse the client rules: {}", self.0.error(e)),
|
||||
}
|
||||
}
|
||||
let mut window_rules = vec![];
|
||||
if let Some(value) = window_rules_val {
|
||||
match value.parse(&mut WindowRulesParser(self.0)) {
|
||||
Ok(v) => window_rules = v,
|
||||
Err(e) => log::warn!("Could not parse the window rules: {}", self.0.error(e)),
|
||||
}
|
||||
}
|
||||
Ok(Config {
|
||||
keymap,
|
||||
repeat_rate,
|
||||
|
|
@ -463,6 +479,7 @@ impl Parser for ConfigParser<'_> {
|
|||
named_actions,
|
||||
max_action_depth,
|
||||
client_rules,
|
||||
window_rules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
113
toml-config/src/config/parsers/window_match.rs
Normal file
113
toml-config/src/config/parsers/window_match.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
GenericMatch, MatchExactly, WindowMatch,
|
||||
context::Context,
|
||||
extractor::{Extractor, ExtractorError, arr, n32, opt, str, val},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
parsers::window_type::{WindowTypeParser, WindowTypeParserError},
|
||||
},
|
||||
toml::{
|
||||
toml_span::{DespanExt, Span, Spanned},
|
||||
toml_value::Value,
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WindowMatchParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error(transparent)]
|
||||
Extract(#[from] ExtractorError),
|
||||
#[error(transparent)]
|
||||
WindowTypes(#[from] WindowTypeParserError),
|
||||
}
|
||||
|
||||
pub struct WindowMatchParser<'a>(pub &'a Context<'a>);
|
||||
|
||||
impl Parser for WindowMatchParser<'_> {
|
||||
type Value = WindowMatch;
|
||||
type Error = WindowMatchParserError;
|
||||
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, types_val),) = ext.extract(((
|
||||
opt(str("name")),
|
||||
opt(val("not")),
|
||||
opt(arr("all")),
|
||||
opt(arr("any")),
|
||||
opt(val("exactly")),
|
||||
opt(val("types")),
|
||||
),))?;
|
||||
let mut not = None;
|
||||
if let Some(value) = not_val {
|
||||
not = Some(Box::new(value.parse(&mut WindowMatchParser(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 WindowMatchParser(self.0))?);
|
||||
}
|
||||
list = Some(res);
|
||||
}
|
||||
list
|
||||
}};
|
||||
}
|
||||
let all = list!(all_val);
|
||||
let any = list!(any_val);
|
||||
let mut types = None;
|
||||
if let Some(value) = types_val {
|
||||
types = Some(value.parse_map(&mut WindowTypeParser)?);
|
||||
}
|
||||
let mut exactly = None;
|
||||
if let Some(value) = exactly_val {
|
||||
exactly = Some(value.parse(&mut WindowMatchExactlyParser(self.0))?);
|
||||
}
|
||||
Ok(WindowMatch {
|
||||
generic: GenericMatch {
|
||||
name: name.despan_into(),
|
||||
not,
|
||||
all,
|
||||
any,
|
||||
exactly,
|
||||
},
|
||||
types,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowMatchExactlyParser<'a>(pub &'a Context<'a>);
|
||||
|
||||
impl Parser for WindowMatchExactlyParser<'_> {
|
||||
type Value = MatchExactly<WindowMatch>;
|
||||
type Error = WindowMatchParserError;
|
||||
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 WindowMatchParser(self.0))?);
|
||||
}
|
||||
Ok(MatchExactly {
|
||||
num: num.value as _,
|
||||
list,
|
||||
})
|
||||
}
|
||||
}
|
||||
104
toml-config/src/config/parsers/window_rule.rs
Normal file
104
toml-config/src/config/parsers/window_rule.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use {
|
||||
crate::{
|
||||
config::{
|
||||
WindowMatch, WindowRule,
|
||||
context::Context,
|
||||
extractor::{Extractor, ExtractorError, opt, str, val},
|
||||
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
parsers::{
|
||||
action::{ActionParser, ActionParserError},
|
||||
window_match::{WindowMatchParser, WindowMatchParserError},
|
||||
},
|
||||
spanned::SpannedErrorExt,
|
||||
},
|
||||
toml::{
|
||||
toml_span::{DespanExt, Span, Spanned},
|
||||
toml_value::Value,
|
||||
},
|
||||
},
|
||||
indexmap::IndexMap,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WindowRuleParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error(transparent)]
|
||||
Extract(#[from] ExtractorError),
|
||||
#[error(transparent)]
|
||||
Match(#[from] WindowMatchParserError),
|
||||
#[error(transparent)]
|
||||
Action(ActionParserError),
|
||||
#[error(transparent)]
|
||||
Latch(ActionParserError),
|
||||
}
|
||||
|
||||
pub struct WindowRuleParser<'a>(pub &'a Context<'a>);
|
||||
|
||||
impl Parser for WindowRuleParser<'_> {
|
||||
type Value = WindowRule;
|
||||
type Error = WindowRuleParserError;
|
||||
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(WindowRuleParserError::Action)?,
|
||||
);
|
||||
}
|
||||
let mut latch = None;
|
||||
if let Some(value) = latch_val {
|
||||
latch = Some(
|
||||
value
|
||||
.parse(&mut ActionParser(self.0))
|
||||
.map_spanned_err(WindowRuleParserError::Latch)?,
|
||||
);
|
||||
}
|
||||
let match_ = match match_val {
|
||||
None => WindowMatch::default(),
|
||||
Some(m) => m.parse_map(&mut WindowMatchParser(self.0))?,
|
||||
};
|
||||
Ok(WindowRule {
|
||||
name: name.despan_into(),
|
||||
match_,
|
||||
action,
|
||||
latch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowRulesParser<'a>(pub &'a Context<'a>);
|
||||
|
||||
impl Parser for WindowRulesParser<'_> {
|
||||
type Value = Vec<WindowRule>;
|
||||
type Error = WindowRuleParserError;
|
||||
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 WindowRuleParser(self.0)) {
|
||||
Ok(o) => res.push(o),
|
||||
Err(e) => {
|
||||
log::warn!("Could not parse window rule: {}", self.0.error(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
53
toml-config/src/config/parsers/window_type.rs
Normal file
53
toml-config/src/config/parsers/window_type.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use {
|
||||
crate::{
|
||||
config::parser::{DataType, ParseResult, Parser, UnexpectedDataType},
|
||||
toml::{
|
||||
toml_span::{Span, Spanned, SpannedExt},
|
||||
toml_value::Value,
|
||||
},
|
||||
},
|
||||
jay_config::{window, window::WindowType},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WindowTypeParserError {
|
||||
#[error(transparent)]
|
||||
Expected(#[from] UnexpectedDataType),
|
||||
#[error("Unknown window type `{}`", .0)]
|
||||
UnknownWindowType(String),
|
||||
}
|
||||
|
||||
pub struct WindowTypeParser;
|
||||
|
||||
impl Parser for WindowTypeParser {
|
||||
type Value = WindowType;
|
||||
type Error = WindowTypeParserError;
|
||||
const EXPECTED: &'static [DataType] = &[DataType::Array, DataType::String];
|
||||
|
||||
fn parse_string(&mut self, span: Span, string: &str) -> ParseResult<Self> {
|
||||
let ty = match string {
|
||||
"none" => WindowType(0),
|
||||
"any" => WindowType(!0),
|
||||
"container" => window::CONTAINER,
|
||||
"placeholder" => window::PLACEHOLDER,
|
||||
"xdg-toplevel" => window::XDG_TOPLEVEL,
|
||||
"x-window" => window::X_WINDOW,
|
||||
"client-window" => window::CLIENT_WINDOW,
|
||||
_ => {
|
||||
return Err(
|
||||
WindowTypeParserError::UnknownWindowType(string.to_owned()).spanned(span)
|
||||
);
|
||||
}
|
||||
};
|
||||
Ok(ty)
|
||||
}
|
||||
|
||||
fn parse_array(&mut self, _span: Span, array: &[Spanned<Value>]) -> ParseResult<Self> {
|
||||
let mut ty = WindowType(0);
|
||||
for el in array {
|
||||
ty |= el.parse(&mut WindowTypeParser)?;
|
||||
}
|
||||
Ok(ty)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ use {
|
|||
config::{
|
||||
Action, ClientRule, Config, ConfigConnector, ConfigDrmDevice, ConfigKeymap,
|
||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, Output, OutputMatch, Shortcut,
|
||||
SimpleCommand, Status, Theme, parse_config,
|
||||
SimpleCommand, Status, Theme, WindowRule, parse_config,
|
||||
},
|
||||
rules::{MatcherTemp, RuleMapper},
|
||||
},
|
||||
|
|
@ -47,6 +47,7 @@ use {
|
|||
on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api,
|
||||
set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode,
|
||||
},
|
||||
window::Window,
|
||||
xwayland::set_x_scaling_mode,
|
||||
},
|
||||
run_on_drop::on_drop,
|
||||
|
|
@ -100,24 +101,39 @@ impl Action {
|
|||
}};
|
||||
}
|
||||
let s = state.persistent.seat;
|
||||
macro_rules! window_or_seat {
|
||||
($name:ident, $expr:expr) => {{
|
||||
let state = state.clone();
|
||||
B::new(move || {
|
||||
if let Some($name) = state.window.get() {
|
||||
if let Some($name) = $name {
|
||||
$expr;
|
||||
}
|
||||
} else {
|
||||
let $name = s;
|
||||
$expr;
|
||||
}
|
||||
})
|
||||
}};
|
||||
}
|
||||
match self {
|
||||
Action::SimpleCommand { cmd } => match cmd {
|
||||
SimpleCommand::Focus(dir) => B::new(move || s.focus(dir)),
|
||||
SimpleCommand::Move(dir) => B::new(move || s.move_(dir)),
|
||||
SimpleCommand::Split(axis) => B::new(move || s.create_split(axis)),
|
||||
SimpleCommand::ToggleSplit => B::new(move || s.toggle_split()),
|
||||
SimpleCommand::SetSplit(b) => B::new(move || s.set_split(b)),
|
||||
SimpleCommand::ToggleMono => B::new(move || s.toggle_mono()),
|
||||
SimpleCommand::SetMono(b) => B::new(move || s.set_mono(b)),
|
||||
SimpleCommand::ToggleFullscreen => B::new(move || s.toggle_fullscreen()),
|
||||
SimpleCommand::SetFullscreen(b) => B::new(move || s.set_fullscreen(b)),
|
||||
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
|
||||
SimpleCommand::Split(axis) => window_or_seat!(s, s.create_split(axis)),
|
||||
SimpleCommand::ToggleSplit => window_or_seat!(s, s.toggle_split()),
|
||||
SimpleCommand::SetSplit(b) => window_or_seat!(s, s.set_split(b)),
|
||||
SimpleCommand::ToggleMono => window_or_seat!(s, s.toggle_mono()),
|
||||
SimpleCommand::SetMono(b) => window_or_seat!(s, s.set_mono(b)),
|
||||
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
|
||||
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
|
||||
SimpleCommand::FocusParent => B::new(move || s.focus_parent()),
|
||||
SimpleCommand::Close => B::new(move || s.close()),
|
||||
SimpleCommand::Close => window_or_seat!(s, s.close()),
|
||||
SimpleCommand::DisablePointerConstraint => {
|
||||
B::new(move || s.disable_pointer_constraint())
|
||||
}
|
||||
SimpleCommand::ToggleFloating => B::new(move || s.toggle_floating()),
|
||||
SimpleCommand::SetFloating(b) => B::new(move || s.set_floating(b)),
|
||||
SimpleCommand::ToggleFloating => window_or_seat!(s, s.toggle_floating()),
|
||||
SimpleCommand::SetFloating(b) => window_or_seat!(s, s.set_floating(b)),
|
||||
SimpleCommand::Quit => B::new(quit),
|
||||
SimpleCommand::ReloadConfigToml => {
|
||||
let persistent = state.persistent.clone();
|
||||
|
|
@ -133,8 +149,10 @@ impl Action {
|
|||
B::new(move || set_float_above_fullscreen(bool))
|
||||
}
|
||||
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::SetFloatPinned(pinned) => {
|
||||
window_or_seat!(s, s.set_float_pinned(pinned))
|
||||
}
|
||||
SimpleCommand::ToggleFloatPinned => window_or_seat!(s, s.toggle_float_pinned()),
|
||||
SimpleCommand::KillClient => client_action!(c, c.kill()),
|
||||
},
|
||||
Action::Multi { actions } => {
|
||||
|
|
@ -153,7 +171,7 @@ impl Action {
|
|||
}
|
||||
Action::MoveToWorkspace { name } => {
|
||||
let workspace = get_workspace(&name);
|
||||
B::new(move || s.set_workspace(workspace))
|
||||
window_or_seat!(s, s.set_workspace(workspace))
|
||||
}
|
||||
Action::ConfigureConnector { con } => B::new(move || {
|
||||
for c in connectors() {
|
||||
|
|
@ -689,6 +707,8 @@ struct State {
|
|||
action_depth: Cell<u64>,
|
||||
|
||||
client: Cell<Option<Client>>,
|
||||
|
||||
window: Cell<Option<Option<Window>>>,
|
||||
}
|
||||
|
||||
impl Drop for State {
|
||||
|
|
@ -897,13 +917,23 @@ impl State {
|
|||
|
||||
fn with_client(&self, client: Client, check: bool, f: impl FnOnce()) {
|
||||
let mut opt = Some(client);
|
||||
if check && client.does_not_exist() {
|
||||
if client.0 == 0 || (check && client.does_not_exist()) {
|
||||
opt = None;
|
||||
}
|
||||
self.client.set(opt);
|
||||
f();
|
||||
self.client.set(None);
|
||||
}
|
||||
|
||||
fn with_window(&self, window: Window, check: bool, f: impl FnOnce()) {
|
||||
let mut w = Some(window);
|
||||
if check && !window.exists() {
|
||||
w = None;
|
||||
}
|
||||
self.window.set(Some(w));
|
||||
f();
|
||||
self.window.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
|
|
@ -922,6 +952,7 @@ struct PersistentState {
|
|||
actions: RefCell<AHashMap<Rc<String>, Rc<dyn Fn()>>>,
|
||||
client_rules: Cell<Vec<MatcherTemp<ClientRule>>>,
|
||||
client_rule_mapper: RefCell<Option<RuleMapper<ClientRule>>>,
|
||||
window_rules: Cell<Vec<MatcherTemp<WindowRule>>>,
|
||||
}
|
||||
|
||||
fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
|
||||
|
|
@ -1003,10 +1034,13 @@ fn load_config(initial_load: bool, persistent: &Rc<PersistentState>) {
|
|||
action_depth_max: config.max_action_depth,
|
||||
action_depth: Cell::new(0),
|
||||
client: Default::default(),
|
||||
window: 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);
|
||||
let (window_rules, _) = state.create_rules(&config.window_rules);
|
||||
persistent.window_rules.set(window_rules);
|
||||
state.set_status(&config.status);
|
||||
persistent.actions.borrow_mut().clear();
|
||||
for a in config.named_actions {
|
||||
|
|
@ -1231,6 +1265,7 @@ pub fn configure() {
|
|||
actions: Default::default(),
|
||||
client_rules: Default::default(),
|
||||
client_rule_mapper: Default::default(),
|
||||
window_rules: Default::default(),
|
||||
});
|
||||
{
|
||||
let p = persistent.clone();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use {
|
||||
crate::{
|
||||
State,
|
||||
config::{ClientMatch, ClientRule, GenericMatch},
|
||||
config::{ClientMatch, ClientRule, GenericMatch, WindowMatch, WindowRule},
|
||||
},
|
||||
ahash::{AHashMap, AHashSet},
|
||||
jay_config::client::{ClientCriterion, ClientMatcher},
|
||||
jay_config::{
|
||||
client::{ClientCriterion, ClientMatcher},
|
||||
window::{WindowCriterion, WindowMatcher},
|
||||
},
|
||||
std::{mem::ManuallyDrop, rc::Rc},
|
||||
};
|
||||
|
||||
|
|
@ -195,6 +198,131 @@ impl Rule for ClientRule {
|
|||
}
|
||||
}
|
||||
|
||||
impl Rule for WindowRule {
|
||||
type Match = WindowMatch;
|
||||
type Matcher = WindowMatcher;
|
||||
type Criterion<'a> = WindowCriterion<'a>;
|
||||
|
||||
const NAME_UPPER: &str = "Window";
|
||||
const NAME_LOWER: &str = "window";
|
||||
|
||||
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<()> {
|
||||
let m = |c: WindowCriterion<'_>| MatcherTemp(c.to_matcher());
|
||||
#[expect(unused_macros)]
|
||||
macro_rules! value {
|
||||
($ty:ident, $field:ident) => {
|
||||
if let Some(value) = &match_.$field {
|
||||
all.push(m(WindowCriterion::$ty(value)));
|
||||
}
|
||||
};
|
||||
}
|
||||
#[expect(unused_macros)]
|
||||
macro_rules! bool {
|
||||
($ty:ident, $field:ident) => {
|
||||
if let Some(value) = &match_.$field {
|
||||
let crit = WindowCriterion::$ty;
|
||||
let matcher = match value {
|
||||
false => m(WindowCriterion::Not(&crit)),
|
||||
true => m(crit),
|
||||
};
|
||||
all.push(matcher);
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(value) = &match_.types {
|
||||
all.push(m(WindowCriterion::Types(*value)));
|
||||
}
|
||||
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, $win:ident) => {
|
||||
let g = $g.clone();
|
||||
let state = state.clone();
|
||||
$win.latch(move || {
|
||||
state.with_client($client, true, || {
|
||||
state.with_window(*$win, 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);
|
||||
matcher.bind(move |win| {
|
||||
let client = win.client();
|
||||
state.with_client(client, false, || {
|
||||
state.with_window(*win, false, &f);
|
||||
});
|
||||
latch!(g, client, win);
|
||||
});
|
||||
} else {
|
||||
matcher.bind(move |win| {
|
||||
let client = win.client();
|
||||
state.with_client(client, false, || {
|
||||
state.with_window(*win, false, &f);
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if let Some(action) = &self.latch {
|
||||
let g = action.clone().into_rc_fn(&state);
|
||||
matcher.bind(move |win| {
|
||||
let client = win.client();
|
||||
latch!(g, client, win);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_matcher(m: Self::Matcher) -> Self::Criterion<'static> {
|
||||
WindowCriterion::Matcher(m)
|
||||
}
|
||||
|
||||
fn gen_not<'a, 'b: 'a>(m: &'a Self::Criterion<'b>) -> Self::Criterion<'a> {
|
||||
WindowCriterion::Not(m)
|
||||
}
|
||||
|
||||
fn gen_all<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
|
||||
WindowCriterion::All(m)
|
||||
}
|
||||
|
||||
fn gen_any<'a, 'b: 'a>(m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
|
||||
WindowCriterion::Any(m)
|
||||
}
|
||||
|
||||
fn gen_exactly<'a, 'b: 'a>(n: usize, m: &'a [Self::Criterion<'b>]) -> Self::Criterion<'a> {
|
||||
WindowCriterion::Exactly(n, m)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RuleMapper<R>
|
||||
where
|
||||
R: Rule,
|
||||
|
|
|
|||
|
|
@ -858,6 +858,14 @@
|
|||
"description": "",
|
||||
"$ref": "#/$defs/ClientRule"
|
||||
}
|
||||
},
|
||||
"windows": {
|
||||
"type": "array",
|
||||
"description": "An array of window rules.\n\nThese rules can be used to give names to windows and to manipulate them.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n action = { type = \"move-to-workspace\", name = \"music\" }\n ```\n",
|
||||
"items": {
|
||||
"description": "",
|
||||
"$ref": "#/$defs/WindowRule"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
|
@ -1487,7 +1495,7 @@
|
|||
},
|
||||
"SimpleActionName": {
|
||||
"type": "string",
|
||||
"description": "The name of a `simple` Action.\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
|
||||
"description": "The name of a `simple` Action.\n\nWhen used inside a window rule, the following actions apply to the matched window\ninstead fo the focused window:\n\n- `move-left`\n- `move-down`\n- `move-up`\n- `move-right`\n- `split-horizontal`\n- `split-vertical`\n- `toggle-split`\n- `tile-horizontal`\n- `tile-vertical`\n- `toggle-split`\n- `show-single`\n- `show-all`\n- `toggle-fullscreen`\n- `enter-fullscreen`\n- `exit-fullscreen`\n- `close`\n- `toggle-floating`\n- `float`\n- `tile`\n- `toggle-float-pinned`\n- `pin-float`\n- `unpin-float`\n\n\n- Example:\n\n ```toml\n [shortcuts]\n alt-q = \"quit\"\n ```\n",
|
||||
"enum": [
|
||||
"focus-left",
|
||||
"focus-down",
|
||||
|
|
@ -1732,6 +1740,115 @@
|
|||
"variant3"
|
||||
]
|
||||
},
|
||||
"WindowMatch": {
|
||||
"description": "Criteria for matching windows.\n\nIf no fields are set, all windows are matched. If multiple fields are set, all fields\nmust match the window.\n",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Matches if the window rule with this name matches.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n\n # Matches the same windows as the previous rule.\n [[windows]]\n match.name = \"spotify\"\n ```\n"
|
||||
},
|
||||
"not": {
|
||||
"description": "Matches if the contained criteria don't match.\n\n- Example:\n\n ```toml\n [[windows]]\n name = \"not-spotify\"\n match.not.title-regex = \"Spotify\"\n ```\n",
|
||||
"$ref": "#/$defs/WindowMatch"
|
||||
},
|
||||
"all": {
|
||||
"type": "array",
|
||||
"description": "Matches if all of the contained criteria match.\n\n- Example:\n\n ```toml\n [[windows]]\n match.all = [\n { title-regex = \"Spotify\" },\n { title-regex = \"Premium\" },\n ]\n ```\n",
|
||||
"items": {
|
||||
"description": "",
|
||||
"$ref": "#/$defs/WindowMatch"
|
||||
}
|
||||
},
|
||||
"any": {
|
||||
"type": "array",
|
||||
"description": "Matches if any of the contained criteria match.\n\n- Example:\n\n ```toml\n [[windows]]\n match.any = [\n { title-regex = \"Spotify\" },\n { title-regex = \"Alacritty\" },\n ]\n ```\n",
|
||||
"items": {
|
||||
"description": "",
|
||||
"$ref": "#/$defs/WindowMatch"
|
||||
}
|
||||
},
|
||||
"exactly": {
|
||||
"description": "Matches if a specific number of contained criteria match.\n\n- Example:\n\n ```toml\n # Matches any window that is either Alacritty or on workspace 3 but not both.\n [[windows]]\n match.exactly.num = 1\n match.exactly.list = [\n { workspace = \"3\" },\n { title-regex = \"Alacritty\" },\n ]\n ```\n",
|
||||
"$ref": "#/$defs/WindowMatchExactly"
|
||||
},
|
||||
"types": {
|
||||
"description": "Matches windows whose type is contained in the mask.",
|
||||
"$ref": "#/$defs/WindowTypeMask"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"WindowMatchExactly": {
|
||||
"description": "Criterion for matching a specific number of window 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/WindowMatch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"num",
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"WindowRule": {
|
||||
"description": "A window 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 [[windows]]\n name = \"spotify\"\n match.title-regex = \"Spotify\"\n\n [[windows]]\n match.name = \"spotify\"\n action = \"enter-fullscreen\"\n ```\n"
|
||||
},
|
||||
"match": {
|
||||
"description": "The criteria that select the window that this rule applies to.",
|
||||
"$ref": "#/$defs/WindowMatch"
|
||||
},
|
||||
"action": {
|
||||
"description": "An action to execute when a window matches the criteria.",
|
||||
"$ref": "#/$defs/Action"
|
||||
},
|
||||
"latch": {
|
||||
"description": "An action to execute when a window no longer matches the criteria.",
|
||||
"$ref": "#/$defs/Action"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"WindowTypeMask": {
|
||||
"description": "A mask of window types.\n",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "A named mask.",
|
||||
"enum": [
|
||||
"none",
|
||||
"any",
|
||||
"container",
|
||||
"xdg-toplevel",
|
||||
"x-window",
|
||||
"client-window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "An array of masks that are OR'd.",
|
||||
"items": {
|
||||
"description": "",
|
||||
"$ref": "#/$defs/WindowTypeMask"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"XScalingMode": {
|
||||
"type": "string",
|
||||
"description": "The scaling mode of X windows.\n\n- Example:\n\n ```toml\n xwayland = { scaling-mode = \"downscaled\" }\n ```\n",
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,23 @@ The table has the following fields:
|
|||
|
||||
The value of this field should be an array of [ClientRules](#types-ClientRule).
|
||||
|
||||
- `windows` (optional):
|
||||
|
||||
An array of window rules.
|
||||
|
||||
These rules can be used to give names to windows and to manipulate them.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
action = { type = "move-to-workspace", name = "music" }
|
||||
```
|
||||
|
||||
The value of this field should be an array of [WindowRules](#types-WindowRule).
|
||||
|
||||
|
||||
<a name="types-Connector"></a>
|
||||
### `Connector`
|
||||
|
|
@ -3235,6 +3252,33 @@ The table has the following fields:
|
|||
|
||||
The name of a `simple` Action.
|
||||
|
||||
When used inside a window rule, the following actions apply to the matched window
|
||||
instead fo the focused window:
|
||||
|
||||
- `move-left`
|
||||
- `move-down`
|
||||
- `move-up`
|
||||
- `move-right`
|
||||
- `split-horizontal`
|
||||
- `split-vertical`
|
||||
- `toggle-split`
|
||||
- `tile-horizontal`
|
||||
- `tile-vertical`
|
||||
- `toggle-split`
|
||||
- `show-single`
|
||||
- `show-all`
|
||||
- `toggle-fullscreen`
|
||||
- `enter-fullscreen`
|
||||
- `exit-fullscreen`
|
||||
- `close`
|
||||
- `toggle-floating`
|
||||
- `float`
|
||||
- `tile`
|
||||
- `toggle-float-pinned`
|
||||
- `pin-float`
|
||||
- `unpin-float`
|
||||
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
|
|
@ -3437,7 +3481,8 @@ The string should have one of the following values:
|
|||
|
||||
Kills a client.
|
||||
|
||||
This action has no effect outside of client rules.
|
||||
Within a window rule, it applies to the client of the window. Within a client rule
|
||||
it applies to the matched client. Has no effect otherwise.
|
||||
|
||||
|
||||
|
||||
|
|
@ -3859,6 +3904,222 @@ The string should have one of the following values:
|
|||
|
||||
|
||||
|
||||
<a name="types-WindowMatch"></a>
|
||||
### `WindowMatch`
|
||||
|
||||
Criteria for matching windows.
|
||||
|
||||
If no fields are set, all windows are matched. If multiple fields are set, all fields
|
||||
must match the window.
|
||||
|
||||
Values of this type should be tables.
|
||||
|
||||
The table has the following fields:
|
||||
|
||||
- `name` (optional):
|
||||
|
||||
Matches if the window rule with this name matches.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
|
||||
# Matches the same windows as the previous rule.
|
||||
[[windows]]
|
||||
match.name = "spotify"
|
||||
```
|
||||
|
||||
The value of this field should be a string.
|
||||
|
||||
- `not` (optional):
|
||||
|
||||
Matches if the contained criteria don't match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "not-spotify"
|
||||
match.not.title-regex = "Spotify"
|
||||
```
|
||||
|
||||
The value of this field should be a [WindowMatch](#types-WindowMatch).
|
||||
|
||||
- `all` (optional):
|
||||
|
||||
Matches if all of the contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
match.all = [
|
||||
{ title-regex = "Spotify" },
|
||||
{ title-regex = "Premium" },
|
||||
]
|
||||
```
|
||||
|
||||
The value of this field should be an array of [WindowMatchs](#types-WindowMatch).
|
||||
|
||||
- `any` (optional):
|
||||
|
||||
Matches if any of the contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
match.any = [
|
||||
{ title-regex = "Spotify" },
|
||||
{ title-regex = "Alacritty" },
|
||||
]
|
||||
```
|
||||
|
||||
The value of this field should be an array of [WindowMatchs](#types-WindowMatch).
|
||||
|
||||
- `exactly` (optional):
|
||||
|
||||
Matches if a specific number of contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
# Matches any window that is either Alacritty or on workspace 3 but not both.
|
||||
[[windows]]
|
||||
match.exactly.num = 1
|
||||
match.exactly.list = [
|
||||
{ workspace = "3" },
|
||||
{ title-regex = "Alacritty" },
|
||||
]
|
||||
```
|
||||
|
||||
The value of this field should be a [WindowMatchExactly](#types-WindowMatchExactly).
|
||||
|
||||
- `types` (optional):
|
||||
|
||||
Matches windows whose type is contained in the mask.
|
||||
|
||||
The value of this field should be a [WindowTypeMask](#types-WindowTypeMask).
|
||||
|
||||
|
||||
<a name="types-WindowMatchExactly"></a>
|
||||
### `WindowMatchExactly`
|
||||
|
||||
Criterion for matching a specific number of window 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 [WindowMatchs](#types-WindowMatch).
|
||||
|
||||
|
||||
<a name="types-WindowRule"></a>
|
||||
### `WindowRule`
|
||||
|
||||
A window 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
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
|
||||
[[windows]]
|
||||
match.name = "spotify"
|
||||
action = "enter-fullscreen"
|
||||
```
|
||||
|
||||
The value of this field should be a string.
|
||||
|
||||
- `match` (optional):
|
||||
|
||||
The criteria that select the window that this rule applies to.
|
||||
|
||||
The value of this field should be a [WindowMatch](#types-WindowMatch).
|
||||
|
||||
- `action` (optional):
|
||||
|
||||
An action to execute when a window matches the criteria.
|
||||
|
||||
The value of this field should be a [Action](#types-Action).
|
||||
|
||||
- `latch` (optional):
|
||||
|
||||
An action to execute when a window no longer matches the criteria.
|
||||
|
||||
The value of this field should be a [Action](#types-Action).
|
||||
|
||||
|
||||
<a name="types-WindowTypeMask"></a>
|
||||
### `WindowTypeMask`
|
||||
|
||||
A mask of window types.
|
||||
|
||||
Values of this type should have one of the following forms:
|
||||
|
||||
#### A string
|
||||
|
||||
A named mask.
|
||||
|
||||
The string should have one of the following values:
|
||||
|
||||
- `none`:
|
||||
|
||||
The empty mask.
|
||||
|
||||
- `any`:
|
||||
|
||||
The mask containing every possible type.
|
||||
|
||||
- `container`:
|
||||
|
||||
The mask matching a container.
|
||||
|
||||
- `xdg-toplevel`:
|
||||
|
||||
The mask matching an XDG toplevel.
|
||||
|
||||
- `x-window`:
|
||||
|
||||
The mask matching an X window.
|
||||
|
||||
- `client-window`:
|
||||
|
||||
The mask matching any type of client window.
|
||||
|
||||
|
||||
#### An array
|
||||
|
||||
An array of masks that are OR'd.
|
||||
|
||||
Each element of this array should be a [WindowTypeMask](#types-WindowTypeMask).
|
||||
|
||||
|
||||
<a name="types-XScalingMode"></a>
|
||||
### `XScalingMode`
|
||||
|
||||
|
|
|
|||
|
|
@ -691,6 +691,33 @@ Exec:
|
|||
SimpleActionName:
|
||||
description: |
|
||||
The name of a `simple` Action.
|
||||
|
||||
When used inside a window rule, the following actions apply to the matched window
|
||||
instead fo the focused window:
|
||||
|
||||
- `move-left`
|
||||
- `move-down`
|
||||
- `move-up`
|
||||
- `move-right`
|
||||
- `split-horizontal`
|
||||
- `split-vertical`
|
||||
- `toggle-split`
|
||||
- `tile-horizontal`
|
||||
- `tile-vertical`
|
||||
- `toggle-split`
|
||||
- `show-single`
|
||||
- `show-all`
|
||||
- `toggle-fullscreen`
|
||||
- `enter-fullscreen`
|
||||
- `exit-fullscreen`
|
||||
- `close`
|
||||
- `toggle-floating`
|
||||
- `float`
|
||||
- `tile`
|
||||
- `toggle-float-pinned`
|
||||
- `pin-float`
|
||||
- `unpin-float`
|
||||
|
||||
|
||||
- Example:
|
||||
|
||||
|
|
@ -825,7 +852,8 @@ SimpleActionName:
|
|||
description: |
|
||||
Kills a client.
|
||||
|
||||
This action has no effect outside of client rules.
|
||||
Within a window rule, it applies to the client of the window. Within a client rule
|
||||
it applies to the matched client. Has no effect otherwise.
|
||||
|
||||
|
||||
Color:
|
||||
|
|
@ -2509,6 +2537,24 @@ Config:
|
|||
name = "spotify"
|
||||
match.sandbox-app-id = "com.spotify.Client"
|
||||
```
|
||||
windows:
|
||||
kind: array
|
||||
items:
|
||||
ref: WindowRule
|
||||
required: false
|
||||
description: |
|
||||
An array of window rules.
|
||||
|
||||
These rules can be used to give names to windows and to manipulate them.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
action = { type = "move-to-workspace", name = "music" }
|
||||
```
|
||||
|
||||
|
||||
Idle:
|
||||
|
|
@ -3284,3 +3330,179 @@ ClientMatchExactly:
|
|||
ref: ClientMatch
|
||||
required: true
|
||||
description: The list of criteria.
|
||||
|
||||
|
||||
WindowRule:
|
||||
kind: table
|
||||
description: |
|
||||
A window rule.
|
||||
fields:
|
||||
name:
|
||||
kind: string
|
||||
required: false
|
||||
description: |
|
||||
The name of this rule.
|
||||
|
||||
This name can be referenced in other rules.
|
||||
|
||||
- Example
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
|
||||
[[windows]]
|
||||
match.name = "spotify"
|
||||
action = "enter-fullscreen"
|
||||
```
|
||||
match:
|
||||
ref: WindowMatch
|
||||
required: false
|
||||
description: The criteria that select the window that this rule applies to.
|
||||
action:
|
||||
ref: Action
|
||||
required: false
|
||||
description: An action to execute when a window matches the criteria.
|
||||
latch:
|
||||
ref: Action
|
||||
required: false
|
||||
description: An action to execute when a window no longer matches the criteria.
|
||||
|
||||
|
||||
WindowMatch:
|
||||
kind: table
|
||||
description: |
|
||||
Criteria for matching windows.
|
||||
|
||||
If no fields are set, all windows are matched. If multiple fields are set, all fields
|
||||
must match the window.
|
||||
fields:
|
||||
name:
|
||||
kind: string
|
||||
required: false
|
||||
description: |
|
||||
Matches if the window rule with this name matches.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "spotify"
|
||||
match.title-regex = "Spotify"
|
||||
|
||||
# Matches the same windows as the previous rule.
|
||||
[[windows]]
|
||||
match.name = "spotify"
|
||||
```
|
||||
not:
|
||||
ref: WindowMatch
|
||||
required: false
|
||||
description: |
|
||||
Matches if the contained criteria don't match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
name = "not-spotify"
|
||||
match.not.title-regex = "Spotify"
|
||||
```
|
||||
all:
|
||||
kind: array
|
||||
items:
|
||||
ref: WindowMatch
|
||||
required: false
|
||||
description: |
|
||||
Matches if all of the contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
match.all = [
|
||||
{ title-regex = "Spotify" },
|
||||
{ title-regex = "Premium" },
|
||||
]
|
||||
```
|
||||
any:
|
||||
kind: array
|
||||
items:
|
||||
ref: WindowMatch
|
||||
required: false
|
||||
description: |
|
||||
Matches if any of the contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
[[windows]]
|
||||
match.any = [
|
||||
{ title-regex = "Spotify" },
|
||||
{ title-regex = "Alacritty" },
|
||||
]
|
||||
```
|
||||
exactly:
|
||||
ref: WindowMatchExactly
|
||||
required: false
|
||||
description: |
|
||||
Matches if a specific number of contained criteria match.
|
||||
|
||||
- Example:
|
||||
|
||||
```toml
|
||||
# Matches any window that is either Alacritty or on workspace 3 but not both.
|
||||
[[windows]]
|
||||
match.exactly.num = 1
|
||||
match.exactly.list = [
|
||||
{ workspace = "3" },
|
||||
{ title-regex = "Alacritty" },
|
||||
]
|
||||
```
|
||||
types:
|
||||
ref: WindowTypeMask
|
||||
required: false
|
||||
description: Matches windows whose type is contained in the mask.
|
||||
|
||||
|
||||
WindowMatchExactly:
|
||||
kind: table
|
||||
description: |
|
||||
Criterion for matching a specific number of window criteria.
|
||||
fields:
|
||||
num:
|
||||
kind: number
|
||||
required: true
|
||||
description: The number of criteria that must match.
|
||||
list:
|
||||
kind: array
|
||||
items:
|
||||
ref: WindowMatch
|
||||
required: true
|
||||
description: The list of criteria.
|
||||
|
||||
|
||||
WindowTypeMask:
|
||||
description: |
|
||||
A mask of window types.
|
||||
kind: variable
|
||||
variants:
|
||||
- kind: string
|
||||
description: A named mask.
|
||||
values:
|
||||
- value: none
|
||||
description: The empty mask.
|
||||
- value: any
|
||||
description: The mask containing every possible type.
|
||||
- value: container
|
||||
description: The mask matching a container.
|
||||
- value: xdg-toplevel
|
||||
description: The mask matching an XDG toplevel.
|
||||
- value: x-window
|
||||
description: The mask matching an X window.
|
||||
- value: client-window
|
||||
description: The mask matching any type of client window.
|
||||
- kind: array
|
||||
description: An array of masks that are OR'd.
|
||||
items:
|
||||
ref: WindowTypeMask
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue