diff options
| -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 |