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 | |
| 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.
| -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 | ||||
| -rw-r--r-- | docs/DEFAULTS.md | 8 | ||||
| -rw-r--r-- | docs/TODO.md | 37 |
7 files changed, 140 insertions, 3 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}'"); diff --git a/docs/DEFAULTS.md b/docs/DEFAULTS.md index ded5b03..e0da5ef 100644 --- a/docs/DEFAULTS.md +++ b/docs/DEFAULTS.md @@ -868,7 +868,13 @@ checks the config directory's `.lua` modification times once a second and reloads when one changes. Set it to `false` for a config that does expensive work at load time. `Mod4+Ctrl+r` still reloads on demand in either case. -**A hand-made change is not undone by a reload.** A reload rebuilds the +**The config file wins for anything it states.** A reload drops any live +`srd set` override for a key the config sets itself, because the file is the +more deliberate statement and has to be able to correct a runtime change. +Without that rule a value changed once at runtime can never be fixed by +editing the config: every reload puts it straight back. + +**A hand-made change is not undone by a reload, where the config is silent.** A reload rebuilds the theme and general settings from the config file, which is right for a file edit but would also wipe every live `srd set` - and the titlebar right-click menu's "Customize" rows are all live `srd set`s. Every setting diff --git a/docs/TODO.md b/docs/TODO.md index e086889..df0bfae 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,42 @@ # TODO / planned features - master checklist +## "Why is there still a tint on windows": a live override the config could never take back (2026-08-28) + +The tint is the drop shadow. `init.lua:65` sets `general.shadows` to +`false`, and loading that same config in a fresh compositor reports +`shadows: false` - but the running session reported `true`. Something had +turned shadows on at runtime, and the session had no way back. + +**That "no way back" is a defect I introduced.** The live-settings replay +added earlier today re-applies every `srd set` after each config reload, so +the titlebar menu's own Customize rows survive a save. The unintended half: +a live override then outranked the config file *permanently*. Editing +`init.lua` and saving could not fix it, because the reload put the override +straight back - which is exactly the state the session was found in. + +Live-always-wins and config-always-wins are both wrong, in opposite +directions. The rule now: **the config wins for anything it states, and a +live override survives only where the config is silent.** That needs a +distinction `values` alone cannot make, since defaults are seeded before any +script runs and so every key looks set - the config engine now records +which keys `srd.set` actually touched during the load, cleared and rebuilt +each time, and restored with everything else if the reload fails. + +Verified end to end, both directions: + + config says shadows=false -> false + srd set shadows true -> true + save an unrelated config edit -> false (the file wins) + + srd set border_width 9 -> 9 (config never mentions it) + save an unrelated config edit -> 9 (override survives) + +The running session was also corrected directly with `srd set shadows +false`, matching what its own config already asked for, so the tint is gone +without waiting for a restart. + +529 tests, clippy clean. + ## Lock screen: the avatar was never read, and the keyboard could not type most passwords (2026-08-28) Two questions, both real gaps. |