srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/config/src/engine/general.rs7
-rw-r--r--crates/config/src/engine/mod.rs17
-rw-r--r--crates/config/src/engine/support.rs4
-rw-r--r--crates/core/src/manager/lock.rs9
-rw-r--r--crates/srdwm/src/main.rs61
-rw-r--r--docs/DEFAULTS.md8
-rw-r--r--docs/TODO.md37
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.