diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/core/src/manager/lock.rs | 16 | ||||
| -rw-r--r-- | crates/core/src/manager/mod.rs | 24 | ||||
| -rw-r--r-- | crates/platform/src/ipc/dispatch.rs | 38 | ||||
| -rw-r--r-- | crates/platform/src/ipc/mod.rs | 1 | ||||
| -rw-r--r-- | crates/platform/src/ipc/tests.rs | 39 | ||||
| -rw-r--r-- | crates/platform/src/lib.rs | 2 | ||||
| -rw-r--r-- | crates/srdwm/src/main.rs | 13 |
7 files changed, 132 insertions, 1 deletions
diff --git a/crates/core/src/manager/lock.rs b/crates/core/src/manager/lock.rs index 0ffe0ca..15c0a9f 100644 --- a/crates/core/src/manager/lock.rs +++ b/crates/core/src/manager/lock.rs @@ -51,6 +51,22 @@ impl WindowManager { pub fn drain_refresh_request(&mut self) -> bool { std::mem::take(&mut self.refresh_requested) } + + /// Records that `key` was set live to `value_json`. See + /// `live_settings`' own doc comment. + /// + /// Last write wins, so setting the same key twice leaves one entry and + /// the replay applies each key exactly once. + pub fn record_live_setting(&mut self, key: &str, value_json: String) { + self.live_settings.insert(key.to_string(), value_json); + } + + /// Every live setting recorded so far, for replay after a config + /// reload. Cloned rather than borrowed: the replay mutates the same + /// `WindowManager` this came from. + pub fn live_settings(&self) -> Vec<(String, String)> { + self.live_settings.iter().map(|(k, v)| (k.clone(), v.clone())).collect() + } } #[cfg(test)] diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs index 7454ce2..e778a32 100644 --- a/crates/core/src/manager/mod.rs +++ b/crates/core/src/manager/mod.rs @@ -437,6 +437,29 @@ pub struct WindowManager { /// Set by `request_refresh`, drained by the main loop. Same /// cross-boundary queued-request shape as `lock_requested`. refresh_requested: bool, + /// Every setting changed live since startup, as `srd set` key -> the + /// raw JSON text of its value, in insertion-independent key order. + /// + /// Exists so a config reload does not silently undo a change the user + /// just made by hand. `apply_general_settings` rebuilds the whole + /// `ThemeConfig` and general-settings block from the config file, which + /// is the correct precedence for a *file* edit - but it also wiped + /// every live `srd set`, and the titlebar right-click menu's own + /// "Customize" section is built entirely out of live `srd set`s. So + /// changing a button style from that menu and then saving `init.lua` + /// for any unrelated reason silently reverted it. + /// + /// That was survivable while reloads only happened on an explicit + /// `Mod4+Ctrl+r`. `general.config_reload_on_write` makes a reload + /// happen on every save, which turns a rare surprise into a reliable + /// one - a control that silently reverts is worse than no control. + /// + /// Raw JSON text rather than a typed value because this crate has no + /// serde dependency and no business gaining one for this; the platform + /// crate parses it back and replays it through the same `handle_set` + /// that recorded it, so a replayed setting cannot diverge from a real + /// one. `BTreeMap` for a deterministic replay order. + live_settings: std::collections::BTreeMap<String, String>, drag: Option<DragState>, resize: Option<ResizeState>, rules: Vec<WindowRule>, @@ -601,6 +624,7 @@ impl WindowManager { theme: ThemeConfig::default(), lock: LockConfig::default(), refresh_requested: false, + live_settings: std::collections::BTreeMap::new(), drag: None, resize: None, rules: Vec::new(), diff --git a/crates/platform/src/ipc/dispatch.rs b/crates/platform/src/ipc/dispatch.rs index d2232c8..749b2aa 100644 --- a/crates/platform/src/ipc/dispatch.rs +++ b/crates/platform/src/ipc/dispatch.rs @@ -437,6 +437,44 @@ pub(crate) fn handle_request(line: &[u8], wm: &std::rc::Rc<std::cell::RefCell<Wi /// predicate is what the two colour/width arms below walk existing /// windows with, rather than touching every window unconditionally. fn handle_set(req: &serde_json::Value, wm: &std::rc::Rc<std::cell::RefCell<WindowManager>>) -> (Vec<u8>, bool) { + let (response, changed) = apply_set(req, wm); + // Recorded only on success, so a rejected value is never replayed -- + // and only for a real client call, not for the replay itself (see + // `replay_live_settings`, which calls `apply_set` directly and would + // otherwise rewrite what it is currently reading). + if changed { + if let (Some(key), Some(value)) = (req.get("key").and_then(|v| v.as_str()), req.get("value")) { + wm.borrow_mut().record_live_setting(key, value.to_string()); + } + } + (response, changed) +} + +/// Re-applies every setting changed live since startup. +/// +/// Called after a config reload, which rebuilds the theme and general +/// settings from the config file and would otherwise silently undo them -- +/// see `WindowManager::live_settings`' own doc comment for why that matters +/// more now that a reload happens on every save. +/// +/// Replayed through `apply_set`, the same function that applied them +/// originally, so a replayed setting cannot behave differently from a real +/// one. Failures are ignored: a value that no longer applies (a monitor +/// that has gone away, say) should not stop the rest being restored. +pub fn replay_live_settings(wm: &std::rc::Rc<std::cell::RefCell<WindowManager>>) -> usize { + let recorded = wm.borrow().live_settings(); + let mut applied = 0; + for (key, raw) in recorded { + let Ok(value) = serde_json::from_str::<serde_json::Value>(&raw) else { continue }; + let req = serde_json::json!({ "cmd": "set", "key": key, "value": value }); + if apply_set(&req, wm).1 { + applied += 1; + } + } + applied +} + +fn apply_set(req: &serde_json::Value, wm: &std::rc::Rc<std::cell::RefCell<WindowManager>>) -> (Vec<u8>, bool) { let key = req.get("key").and_then(|v| v.as_str()).unwrap_or(""); let value = req.get("value"); match key { diff --git a/crates/platform/src/ipc/mod.rs b/crates/platform/src/ipc/mod.rs index 796c459..7094f6e 100644 --- a/crates/platform/src/ipc/mod.rs +++ b/crates/platform/src/ipc/mod.rs @@ -37,6 +37,7 @@ use std::path::PathBuf; use srdwm_core::WindowManager; mod dispatch; +pub use dispatch::replay_live_settings; mod types; #[cfg(test)] mod tests; diff --git a/crates/platform/src/ipc/tests.rs b/crates/platform/src/ipc/tests.rs index c2a678c..2e61761 100644 --- a/crates/platform/src/ipc/tests.rs +++ b/crates/platform/src/ipc/tests.rs @@ -26,6 +26,45 @@ fn read_line(reader: &mut std::io::BufReader<UnixStream>) -> String { } #[test] +fn a_live_setting_is_recorded_and_survives_a_replay() { + // The concrete regression: a config reload rebuilds the theme from the + // file, so a value changed by hand (the titlebar menu's own Customize + // rows are all live `srd set`s) was silently reverted. + let wm = std::rc::Rc::new(std::cell::RefCell::new(WindowManager::new())); + let req = serde_json::json!({"cmd": "set", "key": "button_side", "value": "left"}); + let (_, changed) = super::dispatch::handle_request(serde_json::to_vec(&req).unwrap().as_slice(), &wm); + assert!(changed); + assert!(wm.borrow().theme.buttons_left); + + // Stand in for what a reload does to the theme. + wm.borrow_mut().theme = srdwm_core::ThemeConfig::default(); + assert!(!wm.borrow().theme.buttons_left, "the rebuild really did drop it"); + + assert_eq!(super::replay_live_settings(&wm), 1); + assert!(wm.borrow().theme.buttons_left, "the live change must come back"); +} + +#[test] +fn a_rejected_value_is_never_recorded_for_replay() { + let wm = std::rc::Rc::new(std::cell::RefCell::new(WindowManager::new())); + let req = serde_json::json!({"cmd": "set", "key": "button_side", "value": "sideways"}); + let (_, changed) = super::dispatch::handle_request(serde_json::to_vec(&req).unwrap().as_slice(), &wm); + assert!(!changed, "an invalid value must be rejected"); + assert_eq!(super::replay_live_settings(&wm), 0, "nothing should have been recorded"); +} + +#[test] +fn setting_the_same_key_twice_replays_only_the_last_value() { + let wm = std::rc::Rc::new(std::cell::RefCell::new(WindowManager::new())); + for value in ["left", "right"] { + let req = serde_json::json!({"cmd": "set", "key": "button_side", "value": value}); + super::dispatch::handle_request(serde_json::to_vec(&req).unwrap().as_slice(), &wm); + } + assert_eq!(super::replay_live_settings(&wm), 1, "one entry per key, not one per call"); + assert!(!wm.borrow().theme.buttons_left, "the last value written is the one that survives"); +} + +#[test] fn subscribe_gets_an_immediate_snapshot() { let dir = tempfile::tempdir().unwrap(); let mut server = IpcServer::bind_in(dir.path(), "test").unwrap(); diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index 5de701f..b69aa02 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -12,7 +12,7 @@ mod appmenu_registrar; pub use appmenu_registrar::{AppmenuRegistrarState, RegistrarEvent}; mod ipc; -pub use ipc::IpcServer; +pub use ipc::{replay_live_settings, IpcServer}; #[cfg(unix)] mod pam_auth; diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index e21e585..94da20c 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -798,6 +798,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { Err(e) => report_config_error(&format!("Config edit not applied, keeping the last working one.\n{e}")), } apply_general_settings(&engine, &wm); + // After `apply_general_settings`, which rebuilds the + // theme from the config file - see + // `WindowManager::live_settings` for why a hand-made + // change has to win over that on a *reload*, even + // though the file wins at startup. + let replayed = srdwm_platform::replay_live_settings(&wm); + if replayed > 0 { + log::info!("re-applied {replayed} live setting(s) after the reload"); + } } } } @@ -810,6 +819,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { Ok(()) => log::info!("config reloaded (desktop refresh)"), Err(e) => report_config_error(&format!("Config reload failed, keeping the last working one.\n{e}")), } + apply_general_settings(&engine, &wm); + srdwm_platform::replay_live_settings(&wm); // After the reload, so a handler edited in the config since // startup is the one that runs. engine.dispatch_event("refresh"); @@ -825,6 +836,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { Ok(()) => log::info!("config reloaded"), Err(e) => report_config_error(&format!("Config reload failed, keeping the last working one.\n{e}")), } + apply_general_settings(&engine, &wm); + srdwm_platform::replay_live_settings(&wm); } else if !engine.dispatch_keybinding(&combo) { log::debug!("no binding for '{combo}'"); } |