diff options
| author | srdusr <[email protected]> | 2026-07-05 16:06:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-07-05 16:06:00 +0200 |
| commit | 744757fad22db7a5ebebca9a685cdb00589a1b02 (patch) | |
| tree | 1e73a425a307bae3274dad360b145560ce3e3e27 /crates | |
| parent | d448a82990efb5c510064f1de5b5d32e932a4f9e (diff) | |
| download | srdwm-744757fad22db7a5ebebca9a685cdb00589a1b02.tar.gz srdwm-744757fad22db7a5ebebca9a685cdb00589a1b02.zip | |
Let the config file take back a setting changed at runtime
Reported as windows still being tinted. The tint is the drop shadow, and
init.lua sets general.shadows to false - loading that same config in a
fresh compositor reports false, while the running session reported true.
The reason it could not be corrected is a defect in the live-settings replay
added earlier today. That replay re-applies every srd set after a config
reload so the titlebar menu's Customize rows survive a save. The unintended
half is that a live override then outranked the config file permanently:
editing init.lua and saving put the override straight back, which is the
state the session was found in.
Live-always-wins and config-always-wins are both wrong. The rule is now that
the config wins for anything it states, and a live override survives only
where the config is silent. That distinction cannot come from `values`,
where defaults are seeded before any script runs so every key looks set, so
the config engine records which keys srd.set actually touched during the
load. That record is cleared and rebuilt on each load and restored along
with everything else when a reload fails.
Verified both directions: a config-stated key reverts to the file's value on
the next reload, and a key the config never mentions keeps its live
override.
529 tests pass, clippy clean.
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/config/src/engine/general.rs | 7 | ||||
| -rw-r--r-- | crates/config/src/engine/mod.rs | 17 | ||||
| -rw-r--r-- | crates/config/src/engine/support.rs | 4 | ||||
| -rw-r--r-- | crates/core/src/manager/lock.rs | 9 | ||||
| -rw-r--r-- | crates/srdwm/src/main.rs | 61 |
5 files changed, 96 insertions, 2 deletions
diff --git a/crates/config/src/engine/general.rs b/crates/config/src/engine/general.rs index 68b441e..7c22747 100644 --- a/crates/config/src/engine/general.rs +++ b/crates/config/src/engine/general.rs @@ -8,7 +8,12 @@ impl Engine { let state = self.state.clone(); Ok(self.lua.create_function(move |_, (key, value): (String, Value)| { if let Some(v) = ConfigValue::from_lua(&value) { - state.borrow_mut().values.insert(key, v); + let mut s = state.borrow_mut(); + // Recorded so a reload can tell "the config states this" + // from "this is just the seeded default" - see + // `SharedState::config_set_keys`. + s.config_set_keys.insert(key.clone()); + s.values.insert(key, v); } Ok(()) })?) diff --git a/crates/config/src/engine/mod.rs b/crates/config/src/engine/mod.rs index 8facd56..1dd3c35 100644 --- a/crates/config/src/engine/mod.rs +++ b/crates/config/src/engine/mod.rs @@ -35,6 +35,17 @@ struct SharedState { /// `WindowManager` by `main.rs` and served over IPC as /// `srd keybindings`. key_descriptions: HashMap<String, String>, + /// Every key the *config* set via `srd.set`, as opposed to a seeded + /// default. Cleared and rebuilt on each load, so it always describes + /// the config currently in force. + /// + /// `values` alone cannot answer this: defaults are seeded into it + /// before any script runs, so every key looks set. The distinction + /// matters because a config file stating a value must be able to + /// override a live `srd set` when it is reloaded - otherwise a + /// setting changed at runtime can never be corrected from the config + /// again. + config_set_keys: std::collections::HashSet<String>, /// Combos registered with `srd.bind_repeat`, which fire repeatedly while /// held (Hyprland's `binde`). A subset of `key_bindings`. repeat_keys: std::collections::HashSet<String>, @@ -77,6 +88,7 @@ impl Engine { values: default_config(), key_bindings: HashMap::new(), key_descriptions: HashMap::new(), + config_set_keys: std::collections::HashSet::new(), repeat_keys: std::collections::HashSet::new(), event_handlers: HashMap::new(), config_dir: config_dir.into(), @@ -195,6 +207,11 @@ impl Engine { } } + /// Keys the loaded config explicitly set - see `config_set_keys`. + pub fn config_set_keys(&self) -> Vec<String> { + self.state.borrow().config_set_keys.iter().cloned().collect() + } + pub fn bound_keys(&self) -> Vec<String> { self.state.borrow().key_bindings.keys().cloned().collect() } diff --git a/crates/config/src/engine/support.rs b/crates/config/src/engine/support.rs index 9d81035..b0c8caf 100644 --- a/crates/config/src/engine/support.rs +++ b/crates/config/src/engine/support.rs @@ -34,10 +34,11 @@ pub(super) fn do_reload(lua: &Lua, state: &Rc<RefCell<SharedState>>) -> Result<( std::mem::take(&mut s.event_handlers), std::mem::take(&mut s.repeat_keys), std::mem::take(&mut s.key_descriptions), + std::mem::take(&mut s.config_set_keys), ); (s.config_dir.clone(), previous) }; - let restore = |state: &Rc<RefCell<SharedState>>, previous: (_, _, _, _)| { + let restore = |state: &Rc<RefCell<SharedState>>, previous: (_, _, _, _, _)| { let mut s = state.borrow_mut(); // Whatever the failed run managed to register before erroring is // discarded, not merged: half of a broken config is not a config. @@ -45,6 +46,7 @@ pub(super) fn do_reload(lua: &Lua, state: &Rc<RefCell<SharedState>>) -> Result<( s.event_handlers = previous.1; s.repeat_keys = previous.2; s.key_descriptions = previous.3; + s.config_set_keys = previous.4; }; let path = config_dir.join("init.lua"); let src = match std::fs::read_to_string(&path) { diff --git a/crates/core/src/manager/lock.rs b/crates/core/src/manager/lock.rs index ecea5fe..d24ccae 100644 --- a/crates/core/src/manager/lock.rs +++ b/crates/core/src/manager/lock.rs @@ -73,6 +73,15 @@ impl WindowManager { self.live_settings.insert(key.to_string(), value_json); } + /// Drops the recorded live override for `key`, so the next replay does + /// not reapply it. Used when a reloaded config states that key itself: + /// the file is then the more deliberate statement of intent, and has to + /// win, or a value changed at runtime could never be corrected by + /// editing the config again. + pub fn forget_live_setting(&mut self, key: &str) { + self.live_settings.remove(key); + } + /// 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. diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs index 7670b99..940db50 100644 --- a/crates/srdwm/src/main.rs +++ b/crates/srdwm/src/main.rs @@ -227,6 +227,64 @@ fn publish_gtk_button_layout(wm: &Rc<RefCell<WindowManager>>) { } } +/// `srd set` key -> the config key that states the same thing. +/// +/// Needed so a reload can tell which live overrides the config file itself +/// has an opinion about. Only the keys that exist on both sides appear; a +/// live setting with no config equivalent simply survives every reload. +fn config_key_for_live_setting(key: &str) -> Option<&'static str> { + Some(match key { + "shadows" => "general.shadows", + "rounded_corners" => "general.rounded_corners", + "animations" => "general.animations", + "night_light" => "general.night_light", + "reading_mode" => "general.reading_mode", + "phone_mode" => "general.phone_mode", + "multi_cursor" => "general.multi_cursor", + "per_monitor" => "workspace.per_monitor", + "desktop_icons" => "general.desktop_icons", + "desktop_icons_all_monitors" => "general.desktop_icons_all_monitors", + "close_focus_follows_workspace" => "general.close_focus_follows_workspace", + "gap_inner" => "general.window_gap", + "border_width" => "theme.decorations.border.width", + "border_color" => "theme.decorations.border.active_color", + "corner_radius" => "theme.decorations.border.radius", + "decoration_mode" => "theme.decorations.default_mode", + "button_style" => "theme.decorations.title_bar.button_style", + "button_side" => "theme.decorations.title_bar.button_side", + "button_mode" => "theme.decorations.title_bar.button_mode", + "button_order" => "theme.decorations.title_bar.button_order", + "title_centered" => "theme.decorations.title_bar.text_align", + "button_glyph_always" => "theme.decorations.title_bar.button_glyph", + _ => return None, + }) +} + +/// Drops every live `srd set` override that the freshly-loaded config +/// states for itself, so the config file wins. +/// +/// Live overrides survive a reload by design - otherwise the titlebar +/// menu's own Customize rows would revert every time the config was saved, +/// which with reload-on-write is constantly. But "live always wins" is the +/// opposite failure and a worse one: a value changed once at runtime could +/// never be corrected by editing the config, because every reload put it +/// straight back. Found exactly that way - a session reporting +/// `shadows: true` while its own `init.lua` said `false`, with no way to +/// fix it from the config. +/// +/// So the rule is: the config wins for anything it actually mentions, and a +/// live override survives only where the config is silent. +fn drop_live_settings_the_config_states(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) { + let stated: std::collections::HashSet<String> = engine.config_set_keys().into_iter().collect(); + let live: Vec<String> = wm.borrow().live_settings().into_iter().map(|(k, _)| k).collect(); + let mut wm = wm.borrow_mut(); + for key in live { + if config_key_for_live_setting(&key).is_some_and(|config_key| stated.contains(config_key)) { + wm.forget_live_setting(&key); + } + } +} + /// Copies the loaded config's key bindings into the `WindowManager`, where /// the IPC layer can serve them (`srd keybindings`). /// @@ -964,6 +1022,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { // `WindowManager::live_settings` for why a hand-made // change has to win over that on a *reload*, even // though the file wins at startup. + drop_live_settings_the_config_states(&engine, &wm); let replayed = srdwm_platform::replay_live_settings(&wm); if replayed > 0 { log::info!("re-applied {replayed} live setting(s) after the reload"); @@ -984,6 +1043,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { publish_keybindings(&engine, &wm); publish_gtk_button_layout(&wm); publish_gtk_stylesheet(&wm); + drop_live_settings_the_config_states(&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. @@ -1004,6 +1064,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { publish_keybindings(&engine, &wm); publish_gtk_button_layout(&wm); publish_gtk_stylesheet(&wm); + drop_live_settings_the_config_states(&engine, &wm); srdwm_platform::replay_live_settings(&wm); } else if !engine.dispatch_keybinding(&combo) { log::debug!("no binding for '{combo}'"); |