diff --git a/docs/superpowers/specs/2026-07-16-niri-minimize-design.md b/docs/superpowers/specs/2026-07-16-niri-minimize-design.md index ed88221..fbcf302 100644 --- a/docs/superpowers/specs/2026-07-16-niri-minimize-design.md +++ b/docs/superpowers/specs/2026-07-16-niri-minimize-design.md @@ -226,3 +226,7 @@ the stash holds index 1). - Restoring to a window's *original* workspace. Rejected during design: it needs per-window origin tracking that can go stale. - A waybar module showing a stash count — the workspace pill already signals it. +- Multi-monitor correctness: the stash is pinned to index 1 only on its own + output, so a second monitor's workspaces would start at 1 with no stash, + throwing off the `Mod+1..9` +1 offset there. Correct on this single-panel + (`eDP-1`) machine; latent if a second output is ever attached. diff --git a/niri/config.kdl b/niri/config.kdl index c157971..3922703 100644 --- a/niri/config.kdl +++ b/niri/config.kdl @@ -110,8 +110,8 @@ binds { Mod+Print { spawn "screenshot"; } - // focus=false or focus follows the window into the stash — the opposite of - // minimizing. + // focus=false is required here — otherwise focus follows the window into + // the stash, the opposite of minimizing. Mod+M { move-window-to-workspace "minimized" focus=false; } Mod+Shift+M { spawn "window-restore"; } diff --git a/scripts/tests/window-restore.test.py b/scripts/tests/window-restore.test.py index 9a61ce7..1accc11 100644 --- a/scripts/tests/window-restore.test.py +++ b/scripts/tests/window-restore.test.py @@ -78,6 +78,11 @@ DUPES = [ ] check("duplicate labels resolve to the first", wr.resolve(DUPES, "Alacritty — same")["id"], 20) +# --- window_exists ----------------------------------------------------------- +check("window_exists is True for a present window", wr.window_exists(WINDOWS, 11), True) +check("window_exists is False once the window has closed", wr.window_exists(WINDOWS, 999), False) +check("window_exists is False for an empty list", wr.window_exists([], 11), False) + # --- pick ------------------------------------------------------------------ check("pick returns the chooser's selection", wr.pick(["a", "b"], lambda text: "b"), "b") diff --git a/scripts/window-restore.py b/scripts/window-restore.py index ad757f8..9efa915 100755 --- a/scripts/window-restore.py +++ b/scripts/window-restore.py @@ -50,6 +50,13 @@ def resolve(windows, chosen): return next((w for w in windows if label(w) == chosen), None) +def window_exists(windows, window_id): + """Whether window_id is still present in a fresh `windows` list. Used after + the picker returns, since wofi can block long enough for the chosen window + to have closed out from under us.""" + return any(w.get("id") == window_id for w in windows) + + def pick(labels, chooser): """chooser(text) -> selected line. Injected so tests need no wofi.""" return chooser("\n".join(labels)) @@ -140,6 +147,30 @@ def main(): if window is None: return 0 + # wofi may have sat open for however long the user took. In that time niri + # can destroy a dynamic workspace (its last window closed) and renumber + # every later one down, so the pre-pick `current["idx"]` may now name the + # wrong workspace; the stashed window may also have closed. Re-read state + # rather than trust anything captured before pick() blocked. + workspaces = niri_json("workspaces") + if workspaces is None: + notify("niri IPC unavailable") + return 1 + + current = current_workspace(workspaces) + if current is None: + notify("no focused workspace") + return 1 + + windows = niri_json("windows") + if windows is None: + notify("niri IPC unavailable") + return 1 + + if not window_exists(windows, window["id"]): + notify("that window closed before it could be restored") + return 0 + # The window is not focused (it is in the stash), so --focus does not apply; # focus it explicitly afterwards. niri_action("move-window-to-workspace", "--window-id", str(window["id"]),