diff options
| author | srdusr <[email protected]> | 2026-05-13 20:11:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-05-13 20:11:00 +0200 |
| commit | 8174ced0d9d42343b18072c64491ccb06632a75f (patch) | |
| tree | e838ef26f28447c90440b3e7ff60aad342e0d1f3 | |
| parent | 670c9845129fc0c4b6e0192d657eea05e1a64b49 (diff) | |
| download | srdwm-8174ced0d9d42343b18072c64491ccb06632a75f.tar.gz srdwm-8174ced0d9d42343b18072c64491ccb06632a75f.zip | |
Report whether a listed key binding is actually grabbed
Requested by the AGS session while wiring its launcher to srd keybindings:
without this the launcher would list a shortcut that does nothing and give no
way to tell, which is the same silent-lie class of bug as the rest of the
work today.
The backend is handed one combo list, once, before connecting - X11 turns it
into XGrabKey calls, Wayland into its intercept set. A reload re-registers
the actions but cannot re-register the grabs, so a combination added to the
config since startup is bound as far as the config engine is concerned and
still goes straight to the focused client when pressed. That snapshot is now
recorded on the WindowManager at the exact point it is handed to the
backend, taken once rather than per-arm so the reported set and the grabbed
set cannot drift apart, and each entry in srd keybindings carries a
`grabbed` flag.
Verified live in a nested instance, both states observed: 46 bindings and
46 grabbed at startup; then appending a new combination to the config gave
47 bindings, 46 grabbed, with the new one reported as not grabbed and
carrying its description. Two unit tests cover the set being replaced rather
than accumulated, and a combo outside it reading as not grabbed.
Also recorded, from the AGS session's own checks: moving a window to a
workspace was their bug, not a missing compositor feature - the Overview's
previews had a drop target and the bar's workspace dots had none, so the
gesture worked on one surface and silently did nothing on the other. And the
static half of "are all keybindings working" is clean for the running
session: keybindings.lua was last modified 06:02:55 and the compositor
started 18:11:13, so every combination in it was grabbed at boot.
517 tests pass, clippy clean.
| -rw-r--r-- | crates/core/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/core/src/manager/lock.rs | 34 | ||||
| -rw-r--r-- | crates/core/src/manager/mod.rs | 29 | ||||
| -rw-r--r-- | crates/platform/src/ipc/dispatch.rs | 7 | ||||
| -rw-r--r-- | crates/platform/src/ipc/types.rs | 5 | ||||
| -rw-r--r-- | crates/srdwm/src/main.rs | 27 | ||||
| -rw-r--r-- | docs/DEFAULTS.md | 17 |
7 files changed, 106 insertions, 14 deletions
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index e9d31a5..9a2c3bc 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -19,6 +19,7 @@ pub use layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig}; pub use lock_config::LockConfig; pub use manager::{CaptureRequest, ColorFilter, Direction, WindowManager}; pub use monitor::{Monitor, MonitorId}; +pub use manager::KeyBinding; pub use placement::{centered_in, PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SNAP_FLYOUT_EDGE}; pub use regex::Regex; pub use rules::{WindowMatch, WindowRule, WindowRuleActions}; diff --git a/crates/core/src/manager/lock.rs b/crates/core/src/manager/lock.rs index 15c0a9f..ecea5fe 100644 --- a/crates/core/src/manager/lock.rs +++ b/crates/core/src/manager/lock.rs @@ -52,6 +52,18 @@ impl WindowManager { std::mem::take(&mut self.refresh_requested) } + /// Records the combos the backend was given at startup, so + /// `KeyBinding::grabbed` can be answered. Called once, from `main.rs`, + /// right where that same list is handed to the platform. + pub fn set_grabbed_keys(&mut self, combos: &[String]) { + self.grabbed_keys = combos.iter().cloned().collect(); + } + + /// Whether `combo` is one the backend actually grabs. + pub fn is_grabbed(&self, combo: &str) -> bool { + self.grabbed_keys.contains(combo) + } + /// Records that `key` was set live to `value_json`. See /// `live_settings`' own doc comment. /// @@ -83,6 +95,28 @@ mod tests { } #[test] + fn only_the_combos_handed_to_the_backend_count_as_grabbed() { + // The whole point of the flag: a binding added after startup is + // registered by the config engine but never grabbed, so pressing it + // goes to the focused client instead. + let mut wm = WindowManager::new(); + assert!(!wm.is_grabbed("Mod4+Return"), "nothing grabbed before startup records a set"); + wm.set_grabbed_keys(&["Mod4+Return".to_string(), "Ctrl+Mod4+l".to_string()]); + assert!(wm.is_grabbed("Mod4+Return")); + assert!(wm.is_grabbed("Ctrl+Mod4+l")); + assert!(!wm.is_grabbed("Ctrl+Shift+Mod4+F9"), "added since startup: bound, not grabbed"); + } + + #[test] + fn recording_the_grabbed_set_again_replaces_it_rather_than_accumulating() { + let mut wm = WindowManager::new(); + wm.set_grabbed_keys(&["Mod4+a".to_string()]); + wm.set_grabbed_keys(&["Mod4+b".to_string()]); + assert!(!wm.is_grabbed("Mod4+a"), "the previous set must not linger"); + assert!(wm.is_grabbed("Mod4+b")); + } + + #[test] fn drain_refresh_request_is_true_once_then_false() { let mut wm = WindowManager::new(); assert!(!wm.drain_refresh_request(), "nothing requested yet"); diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs index a225220..64cf5c3 100644 --- a/crates/core/src/manager/mod.rs +++ b/crates/core/src/manager/mod.rs @@ -85,6 +85,29 @@ struct ResizeState { /// state and layout policy. Backends (X11, Wayland, ...) drive this via /// `add_window`/`remove_window`/input events, and apply the `Rect`s it /// computes back onto real surfaces. +/// One key binding, as reported by `srd keybindings`. +#[derive(Debug, Clone)] +pub struct KeyBinding { + /// Canonical form (`"Ctrl+Mod4+l"`), which is what a real keypress is + /// matched against - not necessarily how it was written in the config. + pub combo: String, + /// `srd.bind`'s optional third argument, empty when it gave none. + pub description: String, + /// Whether the compositor actually intercepts this key. + /// + /// `main.rs` hands the backend one combo list, once, before connecting: + /// the X11 backend turns it into `XGrabKey` calls and the Wayland one + /// into its intercept set. A reload re-registers the *actions* but + /// cannot re-register the grabs, so a combination added to the config + /// since startup is bound as far as the config engine is concerned and + /// still goes straight to the focused client when pressed. + /// + /// Reported here so a launcher or cheat-sheet can say so rather than + /// listing a shortcut that silently does nothing - which is exactly + /// the failure this field exists to make visible. + pub grabbed: bool, +} + pub struct WindowManager { windows: HashMap<WindowId, Window>, order: Vec<WindowId>, @@ -449,7 +472,10 @@ pub struct WindowManager { /// Asked for as whether "our bindings show in ags's launcher": they /// could not, because nothing published them anywhere a client could /// read. - pub keybindings: Vec<(String, String)>, + pub keybindings: Vec<KeyBinding>, + /// The exact combo list handed to the backend at startup, which is the + /// only set it ever grabs - see `KeyBinding::grabbed`. + grabbed_keys: std::collections::HashSet<String>, /// Set by `request_refresh`, drained by the main loop. Same /// cross-boundary queued-request shape as `lock_requested`. refresh_requested: bool, @@ -640,6 +666,7 @@ impl WindowManager { theme: ThemeConfig::default(), lock: LockConfig::default(), keybindings: Vec::new(), + grabbed_keys: std::collections::HashSet::new(), maximize_covers_dock: true, refresh_requested: false, live_settings: std::collections::BTreeMap::new(), diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs index ad79b98..b0f0cba 100644 --- a/crates/platform/src/ipc/dispatch.rs +++ b/crates/platform/src/ipc/dispatch.rs @@ -424,8 +424,11 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi // outside this process could see a binding at all before this. "keybindings" => { let wm = wm.borrow(); - let list: Vec<KeybindingInfo> = - wm.keybindings.iter().map(|(combo, description)| KeybindingInfo { combo: combo.clone(), description: description.clone() }).collect(); + let list: Vec<KeybindingInfo> = wm + .keybindings + .iter() + .map(|b| KeybindingInfo { combo: b.combo.clone(), description: b.description.clone(), grabbed: b.grabbed }) + .collect(); (serde_json::to_vec(&KeybindingsResponse { keybindings: list }).unwrap_or_default(), false) } "set" => handle_set(&req, wm), diff --git a/crates/platform/src/ipc/types.rs b/crates/platform/src/ipc/types.rs index 286df20..3243e31 100644 --- a/crates/platform/src/ipc/types.rs +++ b/crates/platform/src/ipc/types.rs @@ -129,6 +129,11 @@ pub(crate) struct MonitorsResponse { pub(crate) struct KeybindingInfo { pub(crate) combo: String, pub(crate) description: String, + /// `false` when the config binds this combo but the compositor does not + /// actually intercept it - see `srdwm_core::KeyBinding::grabbed`. A UI + /// listing bindings should say so rather than showing a shortcut that + /// silently does nothing. + pub(crate) grabbed: bool, } #[derive(Serialize)] diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index 85e5427..6c2f04b 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -111,7 +111,15 @@ const CONFIG_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs /// shows up without a restart - unlike the *grab*, which `main.rs` only /// registers once at startup (see `Engine::reload`'s own doc comment). fn publish_keybindings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) { - wm.borrow_mut().keybindings = engine.bound_keys_with_descriptions(); + let listed = engine.bound_keys_with_descriptions(); + let mut wm = wm.borrow_mut(); + wm.keybindings = listed + .into_iter() + .map(|(combo, description)| { + let grabbed = wm.is_grabbed(&combo); + srdwm_core::KeyBinding { combo, description, grabbed } + }) + .collect(); } /// Puts a config error in front of the user instead of only in a log they @@ -723,20 +731,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { let mut last_config_mtime = config_mtime(&dir); let mut last_config_poll = std::time::Instant::now(); + // One snapshot, handed to whichever backend is built below and also + // recorded on the `WindowManager` so `srd keybindings` can report which + // combos are really grabbed. Taken here rather than inside each arm so + // the reported set and the grabbed set cannot drift apart. + let startup_combos = engine.bound_keys(); + wm.borrow_mut().set_grabbed_keys(&startup_combos); + publish_keybindings(&engine, &wm); let mut platform: Box<dyn Platform> = match kind { #[cfg(all(unix, not(target_os = "macos")))] PlatformKind::X11 => { let mut p = srdwm_x11::X11Platform::connect(wm.clone())?; - let combos = engine.bound_keys(); - p.grab_keybindings(&combos)?; - log::info!("grabbed {} keybinding(s)", combos.len()); + p.grab_keybindings(&startup_combos)?; + log::info!("grabbed {} keybinding(s)", startup_combos.len()); Box::new(p) } #[cfg(all(unix, not(target_os = "macos")))] PlatformKind::Wayland => { - let combos = engine.bound_keys(); - log::info!("{} keybinding(s) will be intercepted from clients", combos.len()); - srdwm_wayland::connect(wm.clone(), &combos, &engine.repeat_keys())? + log::info!("{} keybinding(s) will be intercepted from clients", startup_combos.len()); + srdwm_wayland::connect(wm.clone(), &startup_combos, &engine.repeat_keys())? } #[cfg(windows)] PlatformKind::Windows => Box::new(srdwm_windows::WindowsPlatform::new()?), diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md index abcdad1..daa65d8 100644 --- a/docs/DEFAULTS.md +++ b/docs/DEFAULTS.md @@ -942,9 +942,18 @@ with the window that spawned it. description if it gave one: ``` -{"keybindings":[{"combo":"Mod4+Return","description":"Open a terminal"}, ...]} +{"keybindings":[{"combo":"Mod4+Return","description":"Open a terminal","grabbed":true}, ...]} ``` +`grabbed` is `false` when the config binds that combo but the compositor +does not actually intercept it. The backend is handed one combo list, once, +before connecting - X11 turns it into `XGrabKey` calls, Wayland into its +intercept set. A reload re-registers the *actions* but cannot re-register +the grabs, so a combination added to the config since startup is bound as +far as the config engine is concerned and still goes straight to the focused +client when pressed. Anything listing bindings should show that rather than +offering a shortcut that silently does nothing. + `srd.bind` takes an optional third argument for that description: ```lua @@ -955,9 +964,9 @@ Bindings live entirely inside the Lua engine, so before this nothing outside the compositor could see that a binding existed at all - a launcher or cheat-sheet had no way to show the user their own keys. The list is republished after every config reload, so a rebound key appears without a -restart. Note that a brand new *combination* still needs a restart before the -compositor grabs that key at all; an existing combination picks up its new -action immediately. +restart - with `grabbed: false` until the compositor is restarted, since an +existing combination picks up its new action immediately but a brand new one +is not intercepted until the next startup. Combos are reported in canonical order (`Ctrl+Mod4+l`), which is what the compositor matches a real keypress against - not necessarily how the combo |