aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-02-13 08:45:23 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-02-13 10:30:33 +0300
commita605e7f6227448bbaa18932a251c9cbab9a07686 (patch)
treec7d914ea648e8cbfebba5325f5bf2dfc03c0e950 /src
parent513488f6b8d1ca0a46a2f99d96a57f62dfbf1b5c (diff)
downloadniri-a605e7f6227448bbaa18932a251c9cbab9a07686.tar.gz
niri-a605e7f6227448bbaa18932a251c9cbab9a07686.tar.bz2
niri-a605e7f6227448bbaa18932a251c9cbab9a07686.zip
Implement custom hotkey overlay titles
Diffstat (limited to 'src')
-rw-r--r--src/input/mod.rs8
-rw-r--r--src/ui/hotkey_overlay.rs193
2 files changed, 183 insertions, 18 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs
index d1d564cb..8d9e6d82 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -3009,6 +3009,7 @@ fn should_intercept_key(
// But logically, nothing can inhibit its actions. Only opening it can be
// inhibited.
allow_inhibiting: false,
+ hotkey_overlay_title: None,
});
}
}
@@ -3074,6 +3075,7 @@ fn find_bind(
// It also makes no sense to inhibit the default power key handling.
// Hardcoded binds must never be inhibited.
allow_inhibiting: false,
+ hotkey_overlay_title: None,
});
}
@@ -3541,6 +3543,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
}]);
let comp_mod = CompositorMod::Super;
@@ -3726,6 +3729,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
},
Bind {
key: Key {
@@ -3737,6 +3741,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
},
Bind {
key: Key {
@@ -3748,6 +3753,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
},
Bind {
key: Key {
@@ -3759,6 +3765,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
},
Bind {
key: Key {
@@ -3770,6 +3777,7 @@ mod tests {
cooldown: None,
allow_when_locked: false,
allow_inhibiting: true,
+ hotkey_overlay_title: None,
},
]);
diff --git a/src/ui/hotkey_overlay.rs b/src/ui/hotkey_overlay.rs
index 5a903a3c..c8165ede 100644
--- a/src/ui/hotkey_overlay.rs
+++ b/src/ui/hotkey_overlay.rs
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::rc::Rc;
-use niri_config::{Action, Config, Key, Modifiers, Trigger};
+use niri_config::{Action, Bind, Config, Key, Modifiers, Trigger};
use pangocairo::cairo::{self, ImageSurface};
use pangocairo::pango::{AttrColor, AttrInt, AttrList, AttrString, FontDescription, Weight};
use smithay::backend::renderer::element::Kind;
@@ -125,6 +125,52 @@ impl HotkeyOverlay {
}
}
+fn format_bind(
+ binds: &[Bind],
+ comp_mod: CompositorMod,
+ action: &Action,
+) -> Option<(String, String)> {
+ let mut bind_with_non_null = None;
+ let mut bind_with_custom_title = None;
+ let mut found_null_title = false;
+
+ for bind in binds {
+ if bind.action != *action {
+ continue;
+ }
+
+ match &bind.hotkey_overlay_title {
+ Some(Some(_)) => {
+ bind_with_custom_title.get_or_insert(bind);
+ }
+ Some(None) => {
+ found_null_title = true;
+ }
+ None => {
+ bind_with_non_null.get_or_insert(bind);
+ }
+ }
+ }
+
+ if bind_with_custom_title.is_none() && found_null_title {
+ return None;
+ }
+
+ let mut title = None;
+ let key = if let Some(bind) = bind_with_custom_title.or(bind_with_non_null) {
+ if let Some(Some(custom)) = &bind.hotkey_overlay_title {
+ title = Some(custom.clone());
+ }
+
+ key_name(comp_mod, &bind.key)
+ } else {
+ String::from("(not bound)")
+ };
+ let title = title.unwrap_or_else(|| action_name(action));
+
+ Some((format!(" {key} "), title))
+}
+
fn render(
renderer: &mut GlesRenderer,
config: &Config,
@@ -212,8 +258,17 @@ fn render(
actions.push(&Action::Screenshot);
}
+ // Add actions with a custom hotkey-overlay-title.
+ for bind in binds {
+ if matches!(bind.hotkey_overlay_title, Some(Some(_))) {
+ // Avoid duplicate actions.
+ if !actions.contains(&&bind.action) {
+ actions.push(&bind.action);
+ }
+ }
+ }
+
// Add the spawn actions.
- let mut spawn_actions = Vec::new();
for bind in binds.iter().filter(|bind| {
matches!(bind.action, Action::Spawn(_))
// Only show binds with Mod or Super to filter out stuff like volume up/down.
@@ -225,25 +280,14 @@ fn render(
let action = &bind.action;
// We only show one bind for each action, so we need to deduplicate the Spawn actions.
- if !spawn_actions.contains(&action) {
- spawn_actions.push(action);
+ if !actions.contains(&action) {
+ actions.push(action);
}
}
- actions.extend(spawn_actions);
let strings = actions
.into_iter()
- .map(|action| {
- let key = config
- .binds
- .0
- .iter()
- .find(|bind| bind.action == *action)
- .map(|bind| key_name(comp_mod, &bind.key))
- .unwrap_or_else(|| String::from("(not bound)"));
-
- (format!(" {key} "), action_name(action))
- })
+ .filter_map(|action| format_bind(binds, comp_mod, action))
.collect::<Vec<_>>();
let mut font = FontDescription::from_string(FONT);
@@ -323,8 +367,16 @@ fn render(
cr.rel_move_to((key_width + padding).into(), 0.);
- layout.set_attributes(None);
- layout.set_markup(action);
+ let (attrs, text) = match pango::parse_markup(action, '\0') {
+ Ok((attrs, text, _accel)) => (Some(attrs), text),
+ Err(err) => {
+ warn!("error parsing markup for key {key}: {err}");
+ (None, action.into())
+ }
+ };
+
+ layout.set_attributes(attrs.as_ref());
+ layout.set_text(&text);
pangocairo::functions::show_layout(&cr, &layout);
cr.rel_move_to(
@@ -473,3 +525,108 @@ fn prettify_keysym_name(name: &str) -> String {
name.into()
}
}
+
+#[cfg(test)]
+mod tests {
+ use insta::assert_snapshot;
+
+ use super::*;
+
+ #[track_caller]
+ fn check(config: &str, action: Action) -> String {
+ let config = Config::parse("test.kdl", config).unwrap();
+ if let Some((key, title)) = format_bind(&config.binds.0, CompositorMod::Super, &action) {
+ format!("{key}: {title}")
+ } else {
+ String::from("None")
+ }
+ }
+
+ #[test]
+ fn test_format_bind() {
+ // Not bound.
+ assert_snapshot!(check("", Action::Screenshot), @" (not bound) : Take a Screenshot");
+
+ // Bound with a default title.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" Super + P : Take a Screenshot"
+ );
+
+ // Custom title.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P hotkey-overlay-title="Hello" { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" Super + P : Hello"
+ );
+
+ // Prefer first bind.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P { screenshot; }
+ Print { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" Super + P : Take a Screenshot"
+ );
+
+ // Prefer bind with custom title.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P { screenshot; }
+ Print hotkey-overlay-title="My Cool Bind" { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" PrtSc : My Cool Bind"
+ );
+
+ // Prefer first bind with custom title.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P hotkey-overlay-title="First" { screenshot; }
+ Print hotkey-overlay-title="My Cool Bind" { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" Super + P : First"
+ );
+
+ // Any bind with null title hides it.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P { screenshot; }
+ Print hotkey-overlay-title=null { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @"None"
+ );
+
+ // Custom title takes preference over null.
+ assert_snapshot!(
+ check(
+ r#"binds {
+ Mod+P hotkey-overlay-title="Hello" { screenshot; }
+ Print hotkey-overlay-title=null { screenshot; }
+ }"#,
+ Action::Screenshot,
+ ),
+ @" Super + P : Hello"
+ );
+ }
+}