diff options
| -rw-r--r-- | crates/wayland/src/window_memory.rs | 68 | ||||
| -rw-r--r-- | docs/TODO.md | 47 |
2 files changed, 114 insertions, 1 deletions
diff --git a/crates/wayland/src/window_memory.rs b/crates/wayland/src/window_memory.rs index a92c57c..b2d3711 100644 --- a/crates/wayland/src/window_memory.rs +++ b/crates/wayland/src/window_memory.rs @@ -51,11 +51,54 @@ fn memory_path() -> PathBuf { /// the file doesn't exist yet or is present but unreadable/corrupt - a /// bad state file degrades to "nothing remembered yet", not a startup /// failure. +/// The size `new_managed_window` gives a window before its client has +/// chosen anything: `800 x 600 + TITLEBAR_HEIGHT`. +/// +/// An entry recording exactly this is a size no client ever picked - see +/// `discard_placeholder_entries`. +fn placeholder_size() -> (u32, u32) { + (800, 600 + srdwm_core::TITLEBAR_HEIGHT) +} + +/// Drops entries whose size is exactly the placeholder. +/// +/// `remove_window` no longer records a size the client never chose, so +/// nothing new is captured this way. Entries written before that fix are +/// still on disk, and they are self-perpetuating: a remembered size makes +/// the next launch non-provisional, which forces the client to that size +/// instead of asking it to pick, which writes the same value back on close. +/// An affected app can never escape on its own, so the stale entries have +/// to be dropped rather than waited out. +/// +/// Editing the file by hand does not work, which is worth recording: a +/// running compositor holds the whole table in memory and `save_all` writes +/// all of it back, so a hand-deleted entry reappears at the next save. +/// Filtering on load is the only point where the fix actually sticks. +/// +/// A window genuinely sized exactly 800x632 loses its remembered size once, +/// and gets it back the moment it is next resized or closed at a real size. +/// That is a far smaller cost than an app permanently pinned to a shape it +/// never asked for. +fn discard_placeholder_entries(apps: &mut HashMap<String, PersistedGeometry>) { + let (w, h) = placeholder_size(); + apps.retain(|app_id, g| { + let stale = g.width == w && g.height == h; + if stale { + log::info!("window_memory: dropping {app_id}'s remembered {w}x{h} - that is the placeholder, not a size it chose"); + } + !stale + }); +} + pub(crate) fn load() -> HashMap<String, PersistedGeometry> { let path = memory_path(); let Ok(bytes) = std::fs::read(&path) else { return HashMap::new() }; match serde_json::from_slice::<PersistedWindowMemory>(&bytes) { - Ok(memory) => memory.apps, + Ok(memory) => { + let mut apps = memory.apps; + discard_placeholder_entries(&mut apps); + apps + } Err(e) => { log::warn!("window_memory: couldn't parse {path:?} ({e}); starting with nothing remembered"); HashMap::new() @@ -95,6 +138,29 @@ pub(crate) fn save_all<'a>(entries: impl Iterator<Item = (&'a str, (i32, i32, u3 mod tests { use super::*; + #[test] + fn a_placeholder_sized_entry_is_dropped_on_load() { + // The self-perpetuating case: an app pinned to the size the + // compositor guessed before it had chosen one. + let (w, h) = placeholder_size(); + let mut apps = HashMap::new(); + apps.insert("pinned".to_string(), PersistedGeometry { x: 10, y: 20, width: w, height: h }); + apps.insert("real".to_string(), PersistedGeometry { x: 30, y: 40, width: 1389, height: 933 }); + discard_placeholder_entries(&mut apps); + assert!(!apps.contains_key("pinned"), "the placeholder entry must go"); + assert!(apps.contains_key("real"), "a real remembered size must survive"); + } + + #[test] + fn an_entry_that_merely_shares_one_dimension_is_kept() { + let (w, h) = placeholder_size(); + let mut apps = HashMap::new(); + apps.insert("same_width".to_string(), PersistedGeometry { x: 0, y: 0, width: w, height: h + 1 }); + apps.insert("same_height".to_string(), PersistedGeometry { x: 0, y: 0, width: w + 1, height: h }); + discard_placeholder_entries(&mut apps); + assert_eq!(apps.len(), 2, "only an exact match is the placeholder"); + } + // Only the pure JSON round-trip is exercised here - `state_dir()`/ // `load()`/`save_all()` all touch real environment variables and the // filesystem, which parallel `cargo test` execution can't safely share diff --git a/docs/TODO.md b/docs/TODO.md index df0bfae..e3b973c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,52 @@ # TODO / planned features - master checklist +## The tint is AGS pushing shadows back on, and a store cleanup that could not stick (2026-08-28) + +Four reports. Two have a single cause each, and one of them is not srdwm's. + +**"Firefox has a dark-ish tint when it has focus" - that is the drop +shadow, and AGS turns it on.** `init.lua` sets `general.shadows = false`; +the running session reported `true` again after a restart on a binary that +already had the config-precedence fix. The source is +`ags_project/lib/srdwm.ts:801`, which pushes the shell's own stored value: + + srdSet("shadows", shadows.peek() ? "true" : "false") + +alongside `border_color`, `gap_outer`, `gap_inner` and `corner_radius`. That +is deliberate on the AGS side - propagating one accent pick to every +surface is the point of its theme system - but it means srdwm's config +loses for those keys every time the shell starts. It also explains the +*focus* part exactly: a focused window gets the full `SHADOW_MAX_ALPHA` +while an unfocused one is dimmed by `border_inactive_dim`, so focusing a +window visibly darkens what is behind it. + +Worth flagging as a coordination problem rather than a bug in either half: +srdwm's new rule is that the config wins for keys it states, applied on +every reload, and AGS pushes its own values at startup. Both are reasonable +in isolation and they disagree about the same keys. + +**Window memory: the earlier cleanup was undone the moment it was made.** +Five placeholder-sized entries were deleted from +`window-memory.json` by hand. They came back, because a running compositor +holds the whole table in memory and `save_all` writes all of it back on the +next window close - a hand-edited file cannot survive a running session. + +`load()` now drops entries whose size is exactly the placeholder +(`800 x 600 + TITLEBAR_HEIGHT`), which is the only point where the fix +sticks. `remove_window` already refuses to record such a size, so nothing +new is captured; this clears what was written before that landed. A window +genuinely that size loses its memory once and gets it back on the next real +resize or close - much cheaper than an app permanently pinned to a shape it +never chose. + +**Firefox and Nemo showing no srdwm decorations** is `rules.lua`'s own +`decorated = false` for both, which is the double-titlebar fix working as +intended: srdwm draws neither titlebar nor border, and each app draws its +own chrome. The missing border on Nemo has the same single cause, and the +owner prefers it. + +531 tests, clippy clean. + ## "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 |