diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-23 16:16:52 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-03-23 16:16:52 +0400 |
| commit | 6ec65bc0d6f6141a3951c076b693b6ce430c503a (patch) | |
| tree | 3c601ca9310357935f1c95066797571fcc5a797d | |
| parent | d65446421fb989c74329b053fd3ccc387403fdc5 (diff) | |
| download | niri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.tar.gz niri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.tar.bz2 niri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.zip | |
Add is-focused window rule matcher
| -rw-r--r-- | niri-config/src/lib.rs | 8 | ||||
| -rw-r--r-- | resources/default-config.kdl | 7 | ||||
| -rw-r--r-- | src/niri.rs | 18 | ||||
| -rw-r--r-- | src/window/mapped.rs | 17 | ||||
| -rw-r--r-- | src/window/mod.rs | 23 |
5 files changed, 67 insertions, 6 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 01cdba70..1db7ca41 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -708,11 +708,14 @@ pub struct Match { pub title: Option<Regex>, #[knuffel(property)] pub is_active: Option<bool>, + #[knuffel(property)] + pub is_focused: Option<bool>, } impl PartialEq for Match { fn eq(&self, other: &Self) -> bool { self.is_active == other.is_active + && self.is_focused == other.is_focused && self.app_id.as_ref().map(Regex::as_str) == other.app_id.as_ref().map(Regex::as_str) && self.title.as_ref().map(Regex::as_str) == other.title.as_ref().map(Regex::as_str) } @@ -1766,7 +1769,7 @@ mod tests { window-rule { match app-id=".*alacritty" exclude title="~" - exclude is-active=true + exclude is-active=true is-focused=false open-on-output "eDP-1" open-maximized true @@ -1953,17 +1956,20 @@ mod tests { app_id: Some(Regex::new(".*alacritty").unwrap()), title: None, is_active: None, + is_focused: None, }], excludes: vec![ Match { app_id: None, title: Some(Regex::new("~").unwrap()), is_active: None, + is_focused: None, }, Match { app_id: None, title: None, is_active: Some(true), + is_focused: Some(false), }, ], open_on_output: Some("eDP-1".to_owned()), diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 55bff24d..3409c110 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -368,6 +368,13 @@ animations { // (same as when it uses the active border color). match is-active=true + // Another way to match is by whether the window has keyboard focus. + // This is different from is-active: every workspace on a monitor + // has one active window, but only one window can have a keyboard + // focus. Also, the keyboard focus can go to a layer-shell surface, + // then no window will have the keyboard focus. + match is-focused=true + // Here are the properties that you can set on a window rule. // These properties apply once, when a window first opens. diff --git a/src/niri.rs b/src/niri.rs index ecdacd9a..a05b7d07 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -706,6 +706,24 @@ impl State { focus ); + // Tell the windows their new focus state for window rule purposes. + if let KeyboardFocus::Layout { + surface: Some(surface), + } = &self.niri.keyboard_focus + { + if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { + mapped.set_is_focused(false); + } + } + if let KeyboardFocus::Layout { + surface: Some(surface), + } = &focus + { + if let Some((mapped, _)) = self.niri.layout.find_window_and_output_mut(surface) { + mapped.set_is_focused(true); + } + } + if let Some(grab) = self.niri.popup_grab.as_mut() { if Some(&grab.root) != focus.surface() { trace!( diff --git a/src/window/mapped.rs b/src/window/mapped.rs index 087bf0e7..a48ca636 100644 --- a/src/window/mapped.rs +++ b/src/window/mapped.rs @@ -29,6 +29,9 @@ pub struct Mapped { /// This is not used in all cases; for example, app ID and title changes recompute the rules /// immediately, rather than setting this flag. need_to_recompute_rules: bool, + + /// Whether this window has the keyboard focus. + is_focused: bool, } impl Mapped { @@ -37,6 +40,7 @@ impl Mapped { window, rules, need_to_recompute_rules: false, + is_focused: false, } } @@ -64,6 +68,19 @@ impl Mapped { self.recompute_window_rules(rules) } + + pub fn is_focused(&self) -> bool { + self.is_focused + } + + pub fn set_is_focused(&mut self, is_focused: bool) { + if self.is_focused == is_focused { + return; + } + + self.is_focused = is_focused; + self.need_to_recompute_rules = true; + } } impl LayoutElement for Mapped { diff --git a/src/window/mod.rs b/src/window/mod.rs index 4ff943aa..63cdbe3f 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -61,6 +61,13 @@ impl<'a> WindowRef<'a> { WindowRef::Mapped(mapped) => mapped.toplevel(), } } + + pub fn is_focused(self) -> bool { + match self { + WindowRef::Unmapped(_) => false, + WindowRef::Mapped(mapped) => mapped.is_focused(), + } + } } impl ResolvedWindowRules { @@ -100,13 +107,13 @@ impl ResolvedWindowRules { let mut open_on_output = None; for rule in rules { - if !(rule.matches.is_empty() - || rule.matches.iter().any(|m| window_matches(&role, m))) - { + let matches = |m| window_matches(window, &role, m); + + if !(rule.matches.is_empty() || rule.matches.iter().any(matches)) { continue; } - if rule.excludes.iter().any(|m| window_matches(&role, m)) { + if rule.excludes.iter().any(matches) { continue; } @@ -155,10 +162,16 @@ impl ResolvedWindowRules { } } -fn window_matches(role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool { +fn window_matches(window: WindowRef, role: &XdgToplevelSurfaceRoleAttributes, m: &Match) -> bool { // Must be ensured by the caller. let server_pending = role.server_pending.as_ref().unwrap(); + if let Some(is_focused) = m.is_focused { + if window.is_focused() != is_focused { + return false; + } + } + if let Some(is_active) = m.is_active { // Our "is-active" definition corresponds to the window having a pending Activated state. let pending_activated = server_pending |
