Two fixes found reviewing the paths the acceptance suite couldn't cover.
Show-while-shown-elsewhere used activate_window, which sets
active_monitor_idx and so dragged *focus* to whichever output the window
was parked on. A scratchpad should come to you: if the window is on
another monitor, remove and re-add it floating on the active workspace;
if it is already on the active monitor, focus in place as before.
add_removed_tile_floating dropped the RemovedTile on both of its early
returns (NoOutputs, monitor-not-found), destroying the window while its
client was still alive and leaving it wedged with no surface anywhere.
Both were unreachable via scratchpad_show's guards, but it is a
window-loss footgun for the next caller. It now returns
Result<(), RemovedTile<W>> so callers can stash the tile instead.
Adds two layout tests (they simulate outputs, so multi-monitor is
covered without a second physical display) and corrects the acceptance
suite's test-8 skip message, which claimed the code used active_output
— it never did; it uses active_workspace.
cargo test --lib scratchpad: 10/10. acceptance.py: 8 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stashing a window now stops an active screencast of it, rather than leaving the
consumer on a frozen last frame (a stashed window is never rendered). Covers
both stash paths - Mod+M and Mod+Shift+M's hide branch - by diffing which
windows left the layout across the action, so scratchpad_show's branch logic
isn't duplicated in the caller. Dynamic-target casts are retargeted to Nothing
rather than ended; that carve-out is documented inline.
Also widen the acceptance harness spawn wait to 30s: each test launches its own
nested niri, and a cold alacritty start occasionally exceeded the old 10s.
Deliberately not a retry, which could double-spawn and corrupt window counts.
117 unit tests and 8/8 live acceptance pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review of the destroy-path fix found two Important defects, both now closed:
a stashed window whose client died never stopped its screencast (leaking a
PipeWire stream/portal session nothing could reclaim), and the unmap path had
the identical hole (a stashed window that unmapped was never reinstated into
unmapped_windows, wedging it permanently on re-map). Also corrects a factually
wrong hook-teardown comment. 117 unit tests and 8/8 live acceptance pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds two synthetic multi-output tests (niri's layout harness supports several
outputs) proving scratchpad_show targets the FOCUSED output, plus a control
that it returns to the origin output when focus is unchanged. This is the
substitute for manual multi-monitor verification on a single-display machine.
117 layout tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Replace the workspace-based minimize (declared 'minimized' workspace +
+1 offset + window-restore.py) with a native Sway-style scratchpad built
into niri. Ships as niri-patches/0001-scratchpad.patch + a niri-scratchpad
PKGBUILD. Mod+M stashes; Mod+Shift+M shows/toggles/cycles. Mod+1..9 no
longer offset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design for a Sway-style scratchpad built into niri as a pinned patch set +
niri-scratchpad PKGBUILD. Stores hidden windows in a global stash on Layout
rather than a named workspace, removing the +1 offset and multi-monitor bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mod+M/Mod+Shift+M referenced an undeclared 'minimized' workspace and were silent no-ops. Declare the stash, add focus=false, and add the window-restore wofi picker.
The picker blocks on user input for however long they take. In that
window niri can destroy a dynamic workspace (its last window closed)
and renumber every later one down, so the pre-pick current["idx"] can
point at the wrong workspace by the time move-window-to-workspace
runs — restoring to the wrong place. The stashed window itself can
also have closed in the meantime, in which case the move/focus calls
would silently no-op. main() now re-reads workspaces/windows after
pick() returns, recomputes the current workspace, and confirms the
chosen window still exists before acting; each bails with a notify
and the documented exit code otherwise. Adds window_exists() as a
pure, tested helper alongside the existing pure functions.
Also: clarify the focus=false comment in niri/config.kdl (it read as
if focus=false caused the problem it actually prevents), and note in
the design doc's Out of scope section that the stash's index-1
pinning is per-output, so a second monitor would need its own
analysis of the Mod+1..9 offset.
Mod+M referenced an undeclared 'minimized' workspace, so it silently did
nothing. Declare it, add focus=false so focus does not follow the window
into the stash, and point Mod+Shift+M at the restore picker.
niri pins declared named workspaces to index 1 and will not move them, so
Mod+1..9 are offset by one.
Two tasks: window-restore.py + unit tests, then the niri config declaration,
bind rewiring and install.sh symlink. Script precedes config so Mod+Shift+M is
never wired to a missing script.
Plan code verified by extracting both blocks and running the test suite against
the script: 18/18 pass, and the step-2 failure mode is confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mod+M and Mod+Shift+M reference a "minimized" named workspace that was never
declared, so both binds have always been silent no-ops. Design covers the
declaration plus a wofi restore picker.
Constraints verified against niri 26.04 in a nested instance: undeclared named
workspaces are no-ops, declared ones are pinned to index 1 and cannot be moved
(hence the +1 offset on Mod+1..9), and focus=false is required or focus follows
the window into the stash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Only the H/J/K/L letters were bound, so Mod+Shift+Left and friends did
nothing. Add arrow equivalents next to the existing letter binds for both
focusing and moving; the letter binds are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two effects were hiding the volume slider:
- gamemode-session turns on mako's do-not-disturb mode, whose invisible=1
suppressed every notification, the OSD included.
- niri draws fullscreen windows above the top layer, mako's default, so the
OSD rendered underneath fullscreen windows even outside gamemode.
Tag the OSD with its own app name so mako can match it, then exempt just that
name: invisible=0 to survive DND, layer=overlay to clear fullscreen. The block
is listed after [mode=do-not-disturb] because later criteria win in mako.
Verified with DND active and a fullscreen window: the OSD shows, while another
app's notification under the same conditions stays suppressed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prevents a manually-launched watcher and the spawn-at-startup one from
racing the same winwatch source; the second instance exits immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add scripts/gamemode-watch.py, launched at niri startup, which subscribes
to niri's event stream and toggles gamemode whenever a game window
(steam_app_*, RuneLite, gamescope) is open — so Steam games trigger
gamemode with zero per-game setup instead of needing gamemoderun %command%.
'winwatch' is a third reference-counted source alongside feral + manual.
Pure Watcher class is unit-tested; SIGTERM/finally guarantee cleanup so
the watcher stopping never leaves gamemode stuck on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the new gamemode wiring to the installer; also links mako/config,
which was never linked before (it used to be gitignored).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The eDP-1 panel has no VRR, so the three variable-refresh-rate rules
(including the incorrect is-focused=true one) were no-ops. Removed;
left a breadcrumb comment for a future external VRR monitor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
makoctl/fw-fanctrl echo confirmations to stdout that leaked into add/del
output during integration. run() now discards stdout, keeps stderr.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Un-ignore mako/config and bring it under dotfiles so the gamemode
do-not-disturb mode syncs across machines. [mode=do-not-disturb]
invisible=1 suppresses popups while retaining notification history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hybrid trigger (Feral gamemode hooks + Mod+G toggle), reference-counted
so overlapping sources apply once and revert once. Also removes three
dead VRR window-rules (panel has no VRR support).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Big Picture reuses the existing client window (title change only), so the
open-fullscreen rule never fires on toggle; unused, removing it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add window rules for the Steam client (app-id "steam"): all client
windows float and center, the main library window opens large (75x85%),
and notification toasts sit unobtrusively in the bottom-right without
border or focus-ring. Launched games (steam_app_) are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the hypridle entry to ARCHITECTURE, and folds in the pending
swww->awww wallpaper-tool rename in ARCHITECTURE + README (the repo spawns
awww-daemon, not swww).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drops 'tray' from modules-right and its config block. Network and
bluetooth status remain as their own dedicated modules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hypridle pairs with hyprlock, honors idle inhibitors (video/calls no
longer blank or lock the screen), and is config-file driven. Same staging
as before: blank monitors @5m, lock @10m, suspend-then-hibernate @30m.
Locks via 'loginctl lock-session' so the before-sleep lock is deduplicated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Theme centralization:
- theme/colors.json is now the single source of truth for the palette.
- New theme/generate-theme.py renders, at install time, the three derived
artifacts into ~/.config: colors.css (waybar/wofi @import), mako config,
and hyprlock-colors.conf (hyprlang $tn_* variables).
- theme/colors.css removed from the repo (now generated); install.sh calls
the generator instead of the old inline mako snippet.
- hyprlock.conf sources the generated color file and uses $tn_* variables
instead of hardcoded rgba() values.
- Generator covered by theme/test_generate_theme.py.
Bluetooth:
- Added waybar built-in bluetooth module (config.jsonc + style.css), with
bluez/bluez-utils in packages.txt.
Includes design spec and implementation plan under docs/superpowers/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds /etc/pam.d/greetd to the TARGETS array so re-running
enable-fingerprint.sh extends fprintd auth to the regreet login
screen. Cold-boot timing and silent-touch UX caveats documented
in the spec are accepted as-is — fingerprint failure falls back to
password, no risk of lockout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyprlock doesn't poll fprintd by default — pam_authenticate() is
only invoked on password submission. Adding the auth.fingerprint
block tells hyprlock to launch fprintd polling in parallel with the
password input, so touching the reader unlocks immediately without
typing first.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final-review caveats:
- ARCHITECTURE.md hyprlock section was a generic one-liner inherited
from gtklock; now mentions fprintd integration + GPU + blur.
- hyprlock label blocks didn't set font_family; clock/date now
pinned to JetBrains Mono Nerd Font (same as rest of desktop).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace all gtklock mentions in README, ARCHITECTURE, CLAUDE.md with
hyprlock, and drop the stale gtklock consumer note from theme/colors.css
header comment (hyprlock uses Hyprlang, not CSS imports).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds hyprlock/hyprlock.conf with Tomorrow Night colors, replaces gtklock
with hyprlock in packages.txt, and updates install.sh to symlink
hyprlock.conf into ~/.config/hypr/ instead of the old gtklock symlinks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>