aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-03-23 16:16:52 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-03-23 16:16:52 +0400
commit6ec65bc0d6f6141a3951c076b693b6ce430c503a (patch)
tree3c601ca9310357935f1c95066797571fcc5a797d
parentd65446421fb989c74329b053fd3ccc387403fdc5 (diff)
downloadniri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.tar.gz
niri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.tar.bz2
niri-6ec65bc0d6f6141a3951c076b693b6ce430c503a.zip
Add is-focused window rule matcher
-rw-r--r--niri-config/src/lib.rs8
-rw-r--r--resources/default-config.kdl7
-rw-r--r--src/niri.rs18
-rw-r--r--src/window/mapped.rs17
-rw-r--r--src/window/mod.rs23
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