srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/rules.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-30 16:10:00 +0200
committersrdusr <[email protected]>2024-05-30 16:10:00 +0200
commit1c175642d073689ca11b9252411ea5f8446007d0 (patch)
tree00b8f9062cf1261d2d2642c059a14ff5790522e5 /crates/core/src/rules.rs
parent3d3057ae384ef7389284af8988410889e99c6bb9 (diff)
downloadsrdwm-1c175642d073689ca11b9252411ea5f8446007d0.tar.gz
srdwm-1c175642d073689ca11b9252411ea5f8446007d0.zip
Fix decoration drift during animated transitions; checkpoint IPC/global-menu/output-management work
Border/titlebar decoration was built from Window.geometry (the animation's final target) in both wayland backends' render loops, while sync_geometry already draws a window's actual content at window_anims' interpolated rect during any maximize/fullscreen/open-slide tween. Border and content read two different rectangles for the whole transition, so the border visibly detached from the window it was outlining - reported as "borders aren't flush." Both udev.rs and winit.rs now read the same animated rect for titlebar placement, border-strip placement, and the occlusion test against later windows in stacking order. Verified: cargo build --workspace, cargo clippy (0 new warnings), cargo test -p srdwm-core (111/111). Also checkpoints substantial protocol/IPC work from prior sessions that had accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu; zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct maximized/minimized/fullscreen/activated state per window; output_management (ext-output-management + layer-shell exclusive-zone reservation tracking); workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/ ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a theme module (core/src/theme.rs). A peer session working the AGS shell concurrently verified several of these live against a running srdwm: the global menu rendering a real app's File/Edit menu over gtk-shell1, and foreign-toplevel correctly reporting maximized and fullscreen as independent, non-simultaneous states with the geometry each implies (maximize stops at a reserved top bar and past a dock; fullscreen reaches the true monitor edge).
Diffstat (limited to 'crates/core/src/rules.rs')
-rw-r--r--crates/core/src/rules.rs110
1 files changed, 104 insertions, 6 deletions
diff --git a/crates/core/src/rules.rs b/crates/core/src/rules.rs
index 642179d..148b627 100644
--- a/crates/core/src/rules.rs
+++ b/crates/core/src/rules.rs
@@ -1,22 +1,46 @@
+use regex::Regex;
+
use crate::geometry::Rect;
use crate::window::Window;
use crate::workspace::WorkspaceId;
/// Match criteria for a [`WindowRule`]. A matcher with every field `None`
/// matches nothing (an accidental `srd.rule({}, {...})` in config should be a
-/// silent no-op, not "apply to every window").
+/// silent no-op, not "apply to every window"). Every field that is `Some`
+/// must match (AND semantics) - matching the convention i3's multi-criteria
+/// rules and bspwm's `class:instance:title` rules both already use.
+///
+/// `title_contains`/`class` (plain substring/exact match) are kept
+/// alongside the regex fields below rather than folded into them: they
+/// cover the large majority of real rules (`srd.rule({ class = "firefox" },
+/// ...)`) with no regex syntax to get right, and are cheaper to evaluate.
+/// `title_regex`/`class_regex`/`instance` exist for the cases that need
+/// more precision - disambiguating a specific dialog by title while
+/// leaving an app's main window alone, the concrete example that motivated
+/// adding these - without forcing every simple rule to write one.
#[derive(Debug, Clone, Default)]
pub struct WindowMatch {
/// Case-insensitive substring match against `Window::title`.
pub title_contains: Option<String>,
- /// Case-insensitive exact match against `Window::app_id` (X11 `WM_CLASS`
- /// / Wayland `app_id`).
+ /// Case-insensitive exact match against `Window::app_id` (X11 `WM_CLASS`'s
+ /// *class* half / Wayland `app_id`).
pub class: Option<String>,
+ /// Regex match against `Window::title`. Case-sensitive by default --
+ /// write `(?i)` at the start of the pattern for case-insensitive,
+ /// the same convention i3's own criteria use.
+ pub title_regex: Option<Regex>,
+ /// Regex match against `Window::app_id`.
+ pub class_regex: Option<Regex>,
+ /// Case-insensitive exact match against `Window::instance` (X11
+ /// `WM_CLASS`'s *instance* half). Always fails to match on Wayland-native
+ /// windows, which have no equivalent (`Window::instance` is always
+ /// empty there) - an X11/XWayland-only criterion, same as bspwm's.
+ pub instance: Option<String>,
}
impl WindowMatch {
pub fn is_empty(&self) -> bool {
- self.title_contains.is_none() && self.class.is_none()
+ self.title_contains.is_none() && self.class.is_none() && self.title_regex.is_none() && self.class_regex.is_none() && self.instance.is_none()
}
pub fn matches(&self, window: &Window) -> bool {
@@ -33,6 +57,21 @@ impl WindowMatch {
return false;
}
}
+ if let Some(re) = &self.title_regex {
+ if !re.is_match(&window.title) {
+ return false;
+ }
+ }
+ if let Some(re) = &self.class_regex {
+ if !re.is_match(&window.app_id) {
+ return false;
+ }
+ }
+ if let Some(i) = &self.instance {
+ if !window.instance.eq_ignore_ascii_case(i) {
+ return false;
+ }
+ }
true
}
}
@@ -70,7 +109,7 @@ mod tests {
#[test]
fn title_match_is_case_insensitive_substring() {
let w = Window::new(1, "Mozilla Firefox");
- let m = WindowMatch { title_contains: Some("firefox".into()), class: None };
+ let m = WindowMatch { title_contains: Some("firefox".into()), ..Default::default() };
assert!(m.matches(&w));
}
@@ -78,10 +117,69 @@ mod tests {
fn class_match_is_case_insensitive_exact() {
let mut w = Window::new(1, "");
w.app_id = "Firefox".into();
- let m = WindowMatch { title_contains: None, class: Some("firefox".into()) };
+ let m = WindowMatch { class: Some("firefox".into()), ..Default::default() };
assert!(m.matches(&w));
let mut w2 = Window::new(2, "");
w2.app_id = "firefoxx".into();
assert!(!m.matches(&w2));
}
+
+ #[test]
+ fn title_regex_matches_a_specific_dialog_without_matching_the_main_window() {
+ // The concrete case that motivated adding regex support at all:
+ // disambiguating a specific dialog by title while leaving an app's
+ // main window alone - not reliably possible with substring-only
+ // matching if the dialog's title is a substring-superset situation
+ // (or vice versa) that plain `contains` can't express.
+ let m = WindowMatch { title_regex: Some(Regex::new(r"^Save File$").unwrap()), ..Default::default() };
+ let dialog = Window::new(1, "Save File");
+ let main = Window::new(2, "Save File - GNU Image Manipulation Program");
+ assert!(m.matches(&dialog));
+ assert!(!m.matches(&main));
+ }
+
+ #[test]
+ fn class_regex_is_case_sensitive_unless_the_pattern_opts_in() {
+ let mut w = Window::new(1, "");
+ w.app_id = "firefox".into();
+ let sensitive = WindowMatch { class_regex: Some(Regex::new(r"^Firefox$").unwrap()), ..Default::default() };
+ assert!(!sensitive.matches(&w));
+ let insensitive = WindowMatch { class_regex: Some(Regex::new(r"(?i)^Firefox$").unwrap()), ..Default::default() };
+ assert!(insensitive.matches(&w));
+ }
+
+ #[test]
+ fn instance_match_is_case_insensitive_exact_and_independent_of_class() {
+ let mut w = Window::new(1, "");
+ w.app_id = "Navigator".into();
+ w.instance = "firefox".into();
+ let m = WindowMatch { instance: Some("Firefox".into()), ..Default::default() };
+ assert!(m.matches(&w));
+ let mut w2 = Window::new(2, "");
+ w2.instance = "firefoxdeveloperedition".into();
+ assert!(!m.matches(&w2));
+ }
+
+ #[test]
+ fn multiple_criteria_are_combined_with_and() {
+ let mut w = Window::new(1, "Preferences");
+ w.app_id = "firefox".into();
+ let m = WindowMatch { class: Some("firefox".into()), title_contains: Some("preferences".into()), ..Default::default() };
+ assert!(m.matches(&w));
+ // Same class, different title - must not match once title is
+ // also a criterion.
+ let mut w2 = Window::new(2, "Mozilla Firefox");
+ w2.app_id = "firefox".into();
+ assert!(!m.matches(&w2));
+ }
+
+ #[test]
+ fn a_nonempty_instance_criterion_does_not_match_a_wayland_native_window() {
+ // `Window::instance` is always empty on Wayland (no equivalent
+ // concept), so a real `instance` rule (never an empty-string one --
+ // nobody writes `instance = ""`) must not match there.
+ let w = Window::new(1, "");
+ let m = WindowMatch { instance: Some("firefox".into()), ..Default::default() };
+ assert!(!m.matches(&w));
+ }
}