niri-scratchpad: fix close-while-stashed eviction + add live acceptance test
Refresh the patch with the destroy-path fix (a client dying while stashed is now evicted instead of leaked/zombied), and add pkg/niri-scratchpad/tests/ acceptance.py: a headless IPC-driven regression test that drives a nested patched niri and asserts on `niri msg -j windows`. It found the above bug (unit tests miss it — the harness calls remove_window directly). Run it after rebasing the patch onto a new niri release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,26 @@ index 0aa3cb4f..a5b712e2 100644
|
||||
/// Toggle (open/close) the Overview.
|
||||
ToggleOverview {},
|
||||
/// Open the Overview.
|
||||
diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs
|
||||
index 38440c90..a174921e 100644
|
||||
--- a/src/handlers/xdg_shell.rs
|
||||
+++ b/src/handlers/xdg_shell.rs
|
||||
@@ -834,6 +834,15 @@ impl XdgShellHandler for State {
|
||||
.find_window_and_output(surface.wl_surface());
|
||||
|
||||
let Some((mapped, output)) = win_out else {
|
||||
+ // A window whose client died while stashed in the scratchpad is in no
|
||||
+ // workspace, so find_window_and_output can't see it; evict it here.
|
||||
+ if self
|
||||
+ .niri
|
||||
+ .layout
|
||||
+ .remove_from_scratchpad(surface.wl_surface())
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
// I have no idea how this can happen, but I saw it happen once, in a weird interaction
|
||||
// involving laptop going to sleep and resuming.
|
||||
error!("toplevel missing from both unmapped_windows and layout");
|
||||
diff --git a/src/input/mod.rs b/src/input/mod.rs
|
||||
index f1ad4932..91bc8942 100644
|
||||
--- a/src/input/mod.rs
|
||||
@@ -86,7 +106,7 @@ index f1ad4932..91bc8942 100644
|
||||
self.niri.layout.toggle_overview();
|
||||
self.niri.queue_redraw_all();
|
||||
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
|
||||
index 5c4dd639..077a0570 100644
|
||||
index 5c4dd639..a90d9e6e 100644
|
||||
--- a/src/layout/mod.rs
|
||||
+++ b/src/layout/mod.rs
|
||||
@@ -32,6 +32,7 @@
|
||||
@@ -149,7 +169,7 @@ index 5c4dd639..077a0570 100644
|
||||
if let Some(state) = &self.interactive_move {
|
||||
match state {
|
||||
InteractiveMoveState::Starting { window_id, .. } => {
|
||||
@@ -1204,6 +1222,93 @@ impl<W: LayoutElement> Layout<W> {
|
||||
@@ -1204,6 +1222,106 @@ impl<W: LayoutElement> Layout<W> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -164,6 +184,19 @@ index 5c4dd639..077a0570 100644
|
||||
+ // (Task 4) if this window was the one shown.
|
||||
+ }
|
||||
+
|
||||
+ /// Evict a stashed window from the scratchpad by its surface. Returns true
|
||||
+ /// if one was removed. Needed on the destroy/unmap paths: a window whose
|
||||
+ /// client dies *while stashed* is in no workspace, so `find_window_and_output`
|
||||
+ /// (and therefore `remove_window`) never sees it — this is the only thing
|
||||
+ /// that drops it. A shown scratchpad window lives in a workspace and is
|
||||
+ /// handled by `remove_window` instead.
|
||||
+ pub fn remove_from_scratchpad(&mut self, surface: &WlSurface) -> bool {
|
||||
+ let before = self.scratchpad.len();
|
||||
+ self.scratchpad
|
||||
+ .retain(|rt| !rt.tile.window().is_wl_surface(surface));
|
||||
+ self.scratchpad.len() != before
|
||||
+ }
|
||||
+
|
||||
+ /// Show / toggle / cycle the scratchpad. See the state table in the plan.
|
||||
+ pub fn scratchpad_show(&mut self) {
|
||||
+ // Drop a stale "shown" id (window was closed or moved away).
|
||||
@@ -243,7 +276,7 @@ index 5c4dd639..077a0570 100644
|
||||
pub fn descendants_added(&mut self, id: &W::Id) -> bool {
|
||||
for ws in self.workspaces_mut() {
|
||||
if ws.descendants_added(id) {
|
||||
@@ -2444,6 +2549,24 @@ impl<W: LayoutElement> Layout<W> {
|
||||
@@ -2444,6 +2562,24 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user