diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-02-13 08:45:23 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-02-13 10:30:33 +0300 |
| commit | a605e7f6227448bbaa18932a251c9cbab9a07686 (patch) | |
| tree | c7d914ea648e8cbfebba5325f5bf2dfc03c0e950 | |
| parent | 513488f6b8d1ca0a46a2f99d96a57f62dfbf1b5c (diff) | |
| download | niri-a605e7f6227448bbaa18932a251c9cbab9a07686.tar.gz niri-a605e7f6227448bbaa18932a251c9cbab9a07686.tar.bz2 niri-a605e7f6227448bbaa18932a251c9cbab9a07686.zip | |
Implement custom hotkey overlay titles
| -rw-r--r-- | niri-config/src/lib.rs | 22 | ||||
| -rw-r--r-- | src/input/mod.rs | 8 | ||||
| -rw-r--r-- | src/ui/hotkey_overlay.rs | 193 | ||||
| -rw-r--r-- | wiki/Configuration:-Key-Bindings.md | 39 | ||||
| -rw-r--r-- | wiki/Configuration:-Miscellaneous.md | 3 |
5 files changed, 245 insertions, 20 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 7d65008e..4520d75c 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1334,6 +1334,7 @@ pub struct Bind { pub cooldown: Option<Duration>, pub allow_when_locked: bool, pub allow_inhibiting: bool, + pub hotkey_overlay_title: Option<Option<String>>, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -3232,6 +3233,7 @@ where let mut allow_when_locked = false; let mut allow_when_locked_node = None; let mut allow_inhibiting = true; + let mut hotkey_overlay_title = None; for (name, val) in &node.properties { match &***name { "repeat" => { @@ -3249,6 +3251,9 @@ where "allow-inhibiting" => { allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?; } + "hotkey-overlay-title" => { + hotkey_overlay_title = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?); + } name_str => { ctx.emit_error(DecodeError::unexpected( name, @@ -3271,6 +3276,7 @@ where cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }; if let Some(child) = children.next() { @@ -3306,6 +3312,7 @@ where cooldown, allow_when_locked, allow_inhibiting, + hotkey_overlay_title, }) } Err(e) => { @@ -3707,10 +3714,10 @@ mod tests { } binds { - Mod+Escape { toggle-keyboard-shortcuts-inhibit; } + Mod+Escape hotkey-overlay-title="Inhibit" { toggle-keyboard-shortcuts-inhibit; } Mod+Shift+Escape allow-inhibiting=true { toggle-keyboard-shortcuts-inhibit; } Mod+T allow-when-locked=true { spawn "alacritty"; } - Mod+Q { close-window; } + Mod+Q hotkey-overlay-title=null { close-window; } Mod+Shift+H { focus-monitor-left; } Mod+Ctrl+Shift+L { move-window-to-monitor-right; } Mod+Comma { consume-window-into-column; } @@ -4055,6 +4062,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: false, + hotkey_overlay_title: Some(Some("Inhibit".to_owned())), }, Bind { key: Key { @@ -4066,6 +4074,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: false, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4077,6 +4086,7 @@ mod tests { cooldown: None, allow_when_locked: true, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4088,6 +4098,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: Some(None), }, Bind { key: Key { @@ -4099,6 +4110,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4110,6 +4122,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4121,6 +4134,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4132,6 +4146,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4145,6 +4160,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4156,6 +4172,7 @@ mod tests { cooldown: None, allow_when_locked: false, allow_inhibiting: false, + hotkey_overlay_title: None, }, Bind { key: Key { @@ -4167,6 +4184,7 @@ mod tests { cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, allow_inhibiting: true, + hotkey_overlay_title: None, }, ]), switch_events: SwitchBinds { 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" + ); + } +} diff --git a/wiki/Configuration:-Key-Bindings.md b/wiki/Configuration:-Key-Bindings.md index 2331f8a9..27ad7692 100644 --- a/wiki/Configuration:-Key-Bindings.md +++ b/wiki/Configuration:-Key-Bindings.md @@ -119,6 +119,45 @@ Mouse clicks operate on the window that was focused at the time of the click, no Note that binding `Mod+MouseLeft` or `Mod+MouseRight` will override the corresponding gesture (moving or resizing the window). +### Custom Hotkey Overlay Titles + +<sup>Since: next release</sup> + +The hotkey overlay (the Important Hotkeys dialog) shows a hardcoded list of binds. +You can customize this list using the `hotkey-overlay-title` property. + +To add a bind to the hotkey overlay, set the property to the title that you want to show: +```kdl +binds { + Mod+Shift+S hotkey-overlay-title="Toggle Dark/Light Style" { spawn "some-script.sh"; } +} +``` + +Binds with custom titles are listed after the hardcoded binds and before non-customized Spawn binds. + +To remove a hardcoded bind from the hotkey overlay, set the property to null: +```kdl +binds { + Mod+Q hotkey-overlay-title=null { close-window; } +} +``` + +> [!TIP] +> When multiple key combinations are bound to the same action: +> - If any of the binds has a custom hotkey overlay title, niri will show that bind. +> - Otherwise, if any of the binds has a null title, niri will hide the bind. +> - Otherwise, niri will show the first key combination. + +Custom titles support [Pango markup](https://docs.gtk.org/Pango/pango_markup.html): + +```kdl +binds { + Mod+Shift+S hotkey-overlay-title="<b>Toggle</b> <span foreground='red'>Dark</span>/Light Style" { spawn "some-script.sh"; } +} +``` + + + ### Actions Every action that you can bind is also available for programmatic invocation via `niri msg action`. diff --git a/wiki/Configuration:-Miscellaneous.md b/wiki/Configuration:-Miscellaneous.md index 3107d304..fe97d310 100644 --- a/wiki/Configuration:-Miscellaneous.md +++ b/wiki/Configuration:-Miscellaneous.md @@ -166,3 +166,6 @@ hotkey-overlay { skip-at-startup } ``` + +You can customize which binds the hotkey overlay shows using the `hotkey-overlay-title` property. +Check the [key bindings](./Configuration:-Key-Bindings.md) wiki page for details. |
