srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-08-05 00:55:00 +0200
committersrdusr <[email protected]>2026-08-05 00:55:00 +0200
commit9c1673fa73bb49433370a60a7b4bb16abee98a9b (patch)
treed67c4e6187aee0229944900de9c1bf5c6095d20c
parent3c47912ae0a2b81d06e4e9101b5b4e2f1e2997d9 (diff)
downloadsrdwm-9c1673fa73bb49433370a60a7b4bb16abee98a9b.tar.gz
srdwm-9c1673fa73bb49433370a60a7b4bb16abee98a9b.zip
Show a keybinding the way a person writes it, and let bind_repeat be described
Measured with dotfiles-1a, who own the launcher that displays these: `srd keybindings` returned 84 bindings and 84 empty descriptions - 100% - so every entry their launcher showed was a bare key combo with nothing to say what it does. Two causes, one on each side of the boundary. `srd.bind_repeat` never accepted a description. `srd.bind` has taken an optional third argument all along, but its repeating sibling took only two, and mlua drops a surplus argument silently rather than raising - so a config that documented its repeat bindings got no error and no description. It now takes one exactly like `bind`. The combos themselves were reported in their internal dispatch form: `Shift+Mod4+h`. `Mod4` is the X11 modifier's name, not a key's; nothing on a keyboard is labelled Mod4, and the canonical Ctrl/Shift/Alt/Mod4 ordering renders the owner's own `Super+Shift+h` binding back to them inside out. `srd keybindings` now reports a display form - Super, and the order people write - while everything internal keeps the canonical form it dispatches on. The display form parses back to the same binding, so it can be pasted into a config, and a test pins that round trip rather than trusting it. The owner's own config now describes all 84 bindings; verified through the real path, in a nested compositor running that config: 84 of 84 described, zero occurrences of "Mod4".
-rw-r--r--crates/config/src/engine/general.rs10
-rw-r--r--crates/core/src/event.rs52
-rw-r--r--crates/core/src/lib.rs2
-rw-r--r--crates/platform/src/ipc/dispatch.rs12
4 files changed, 73 insertions, 3 deletions
diff --git a/crates/config/src/engine/general.rs b/crates/config/src/engine/general.rs
index 7c22747..05f0f66 100644
--- a/crates/config/src/engine/general.rs
+++ b/crates/config/src/engine/general.rs
@@ -102,10 +102,18 @@ impl Engine {
/// and window-switcher cycling, where one step per press is unusable.
pub(super) fn fn_bind_repeat(&self) -> Result<mlua::Function<'_>> {
let state = self.state.clone();
- Ok(self.lua.create_function(move |lua, (combo, f): (String, mlua::Function)| {
+ Ok(self.lua.create_function(move |lua, (combo, f, description): (String, mlua::Function, Option<String>)| {
let combo = srdwm_core::canonicalize_key_combo(&combo);
let key = lua.create_registry_value(f)?;
let mut s = state.borrow_mut();
+ // Takes a description exactly like `bind` does. It did not, and
+ // mlua drops the extra argument silently rather than raising --
+ // so a config that documented its repeat bindings got no error
+ // and no description, and every one of them showed up in a
+ // launcher as a bare key combo.
+ if let Some(description) = description.filter(|d| !d.trim().is_empty()) {
+ s.key_descriptions.insert(combo.clone(), description);
+ }
s.repeat_keys.insert(combo.clone());
s.key_bindings.insert(combo, key);
Ok(())
diff --git a/crates/core/src/event.rs b/crates/core/src/event.rs
index 0bfadc0..f171f06 100644
--- a/crates/core/src/event.rs
+++ b/crates/core/src/event.rs
@@ -104,6 +104,37 @@ pub fn parse_key_combo(combo: &str) -> Option<(Modifiers, &str)> {
Some((modifiers, key_name))
}
+/// The form of a combo to *show a person*, as distinct from the canonical
+/// form everything internal is keyed by.
+///
+/// Two differences, both of which matter only on screen:
+///
+/// - `Mod4` is written `Super`. `Mod4` is the X11 modifier's name, not the
+/// key's; nothing on a keyboard is labelled Mod4, and a launcher listing
+/// "Mod4+Return" is showing an implementation detail.
+/// - Modifiers come out in the order people write them - Super, Ctrl, Alt,
+/// Shift - rather than the canonical Ctrl/Shift/Alt/Mod4 order, which
+/// renders the owner's own `Super+Shift+h` binding as `Shift+Mod4+h`.
+///
+/// Round-trips: [`parse_key_combo`] accepts `Super` and takes modifiers in
+/// any order, so a string from here can be pasted straight into a config.
+/// Nothing dispatches on this form - [`canonicalize_key_combo`] still owns
+/// what a binding is stored and looked up as.
+pub fn display_key_combo(combo: &str) -> String {
+ let Some((modifiers, key_name)) = parse_key_combo(combo) else { return combo.to_string() };
+ let mut out = String::new();
+ for (flag, name) in
+ [(Modifiers::SUPER, "Super"), (Modifiers::CTRL, "Ctrl"), (Modifiers::ALT, "Alt"), (Modifiers::SHIFT, "Shift")]
+ {
+ if modifiers.contains(flag) {
+ out.push_str(name);
+ out.push('+');
+ }
+ }
+ out.push_str(key_name);
+ out
+}
+
/// Re-orders a combo string into the canonical form [`key_combo_string`]
/// produces, regardless of what order its modifiers were written in, *and*
/// normalizes the key name to the exact casing [`crate::keysyms::
@@ -180,6 +211,27 @@ pub enum Event {
#[cfg(test)]
mod tests {
+ /// A launcher shows this string to a person, so it has to read like the
+ /// key on the keyboard and like what the config author wrote.
+ #[test]
+ fn display_form_uses_super_and_the_order_people_write() {
+ assert_eq!(display_key_combo("Shift+Mod4+h"), "Super+Shift+h");
+ assert_eq!(display_key_combo("Ctrl+Mod4+k"), "Super+Ctrl+k");
+ assert_eq!(display_key_combo("Shift+Alt+Tab"), "Alt+Shift+Tab");
+ assert_eq!(display_key_combo("Mod4+Return"), "Super+Return");
+ assert_eq!(display_key_combo("Print"), "Print");
+ }
+
+ /// The display form must be pasteable back into a config, or telling
+ /// someone their binding is "Super+Shift+h" sends them to a string that
+ /// does not work.
+ #[test]
+ fn display_form_parses_back_to_the_same_binding() {
+ for combo in ["Shift+Mod4+h", "Ctrl+Mod4+k", "Shift+Alt+Tab", "Mod4+Return", "Mod4+1"] {
+ assert_eq!(canonicalize_key_combo(&display_key_combo(combo)), canonicalize_key_combo(combo));
+ }
+ }
+
use super::*;
#[test]
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index 9a2c3bc..755a00e 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -13,7 +13,7 @@ pub mod window;
pub mod workspace;
pub use context_menu::{ContextMenu, MenuAction};
-pub use event::{canonicalize_key_combo, key_combo_string, parse_key_combo, Event, MouseButton, Modifiers};
+pub use event::{canonicalize_key_combo, display_key_combo, key_combo_string, parse_key_combo, Event, MouseButton, Modifiers};
pub use geometry::Rect;
pub use layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig};
pub use lock_config::LockConfig;
diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs
index b0f0cba..49d8244 100644
--- a/crates/platform/src/ipc/dispatch.rs
+++ b/crates/platform/src/ipc/dispatch.rs
@@ -427,7 +427,17 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi
let list: Vec<KeybindingInfo> = wm
.keybindings
.iter()
- .map(|b| KeybindingInfo { combo: b.combo.clone(), description: b.description.clone(), grabbed: b.grabbed })
+ // `display_key_combo`, not the stored combo: what is stored
+ // is the canonical dispatch key (`Shift+Mod4+h`), and a
+ // panel or launcher showing that verbatim is showing an X11
+ // modifier name nothing on a keyboard is labelled with. The
+ // form here is what the owner wrote in their own config, and
+ // parses back to the same binding.
+ .map(|b| KeybindingInfo {
+ combo: srdwm_core::display_key_combo(&b.combo),
+ description: b.description.clone(),
+ grabbed: b.grabbed,
+ })
.collect();
(serde_json::to_vec(&KeybindingsResponse { keybindings: list }).unwrap_or_default(), false)
}