atagen

monolith

public
Star0
Watch0

Commit 01888dfc01d4

tarball

atagen <boss@atagen.co>2026-10-19 08:55 UTC

3 files changed+66-9
rules: match X11 class and instance, dialogs and parents

map_window passed the rules only app_id and title, so the matchers in the config had nothing to test. Facts gathers class and instance from X11Surface, a dialog flag from the xdg parent or X11 transient and type, and the parent window's app_id.
Modified file: crates/monolith/src/main.rs+1 additions
@@ -22,6 +22,7 @@ mod nested;
2222 mod pacing;
2323 mod popups;
2424 mod power;
25+mod rules;
2526 mod scale;
2627 mod scene;
2728 mod seat;
Added file: crates/monolith/src/rules.rs+62 additions
@@ -0,0 +1,62 @@
1+//! Window rules. They see a window through `Facts` and are evaluated when it
2+//! maps and again when its title or app_id changes.
3+
4+use crate::state::State;
5+use crate::wm::identity;
6+use monolith_config::Subject;
7+use smithay::desktop::Window;
8+use smithay::xwayland::xwm::WmWindowType;
9+
10+/// What rules can tell about a window, owned so it can be built before the
11+/// window has a record.
12+pub struct Facts {
13+ app_id: String,
14+ title: String,
15+ initial_title: String,
16+ x11: Option<(String, String)>,
17+ dialog: bool,
18+ parent_app_id: Option<String>,
19+}
20+
21+impl Facts {
22+ pub fn subject(&self) -> Subject<'_> {
23+ Subject {
24+ app_id: &self.app_id,
25+ title: &self.title,
26+ initial_title: &self.initial_title,
27+ class: self.x11.as_ref().map(|(c, _)| c.as_str()),
28+ instance: self.x11.as_ref().map(|(_, i)| i.as_str()),
29+ xwayland: self.x11.is_some(),
30+ dialog: self.dialog,
31+ parent_app_id: self.parent_app_id.as_deref(),
32+ }
33+ }
34+}
35+
36+impl State {
37+ /// The facts about `window` now. `initial_title` is the title it mapped
38+ /// with, None for a window that is only now mapping.
39+ pub fn facts(&self, window: &Window, initial_title: Option<&str>) -> Facts {
40+ let (app_id, title) = identity(window);
41+ let x11 = window.x11_surface();
42+ let dialog = match (x11, window.toplevel()) {
43+ (Some(x), _) => {
44+ x.is_transient_for().is_some() || x.window_type() == Some(WmWindowType::Dialog)
45+ }
46+ (_, Some(t)) => t.parent().is_some(),
47+ _ => false,
48+ };
49+ let parent_app_id = self
50+ .parent_of(window)
51+ .and_then(|p| self.wm.get(p))
52+ .map(|p| p.app_id.clone());
53+ Facts {
54+ app_id,
55+ initial_title: initial_title.map_or_else(|| title.clone(), str::to_owned),
56+ title,
57+ x11: x11.map(|x| (x.class(), x.instance())),
58+ dialog,
59+ parent_app_id,
60+ }
61+ }
62+}
Modified file: crates/monolith/src/windows.rs+3 additions−9 deletions
@@ -1,7 +1,6 @@
11 use crate::anim::Ghost;
22 use crate::state::{State, now_ns};
33 use crate::wm::{Kind, LastFrame, identity, root};
4-use monolith_config::Subject;
54 use monolith_layout::{OutputId, Rect, WindowId};
65 use monolith_render_vk::VulkanTexture;
76 use smithay::backend::renderer::Renderer;
@@ -21,13 +20,8 @@ impl State {
2120 /// and title are known and the initial configure can carry its size.
2221 pub fn map_window(&mut self, window: Window, float: Option<Rectangle<i32, Logical>>) {
2322 let (app_id, title) = identity(&window);
24- let subject = Subject {
25- app_id: &app_id,
26- title: &title,
27- initial_title: &title,
28- ..Subject::default()
29- };
30- let rule = self.settings.config.placement(&subject);
23+ let facts = self.facts(&window, None);
24+ let rule = self.settings.config.placement(&facts.subject());
3125 let launch = self.launch_of(self.window_pid(&window));
3226 let (fullscreen, fullscreen_output) = requested_fullscreen(&window);
3327 let output = rule
@@ -91,7 +85,7 @@ impl State {
9185 }
9286
9387 /// The mapped window `window` is a dialog or other transient of.
94- fn parent_of(&self, window: &Window) -> Option<WindowId> {
88+ pub(crate) fn parent_of(&self, window: &Window) -> Option<WindowId> {
9589 if let Some(x) = window.x11_surface() {
9690 let parent = x.is_transient_for()?;
9791 return self