You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(hammerspoon): make ⌥0 consolidation work on macOS 26 (Tahoe)
moveWindowToSpace is a no-op on macOS 26 (returns true, moves nothing),
and allWindows() only enumerates the current Space, so the first cut
found 0 windows and moved nothing. Rebuild on mechanisms that work on
Tahoe: enumerate every Space via an all-Spaces hs.window.filter,
un-full-screen those windows, then removeSpace every user Space except
each screen's focused one (removeSpace migrates a Space's windows onto a
neighbour, funnelling everything onto the survivor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .agents/skills/hammerspoon/SKILL.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,7 +11,8 @@ Hammerspoon provides direct macOS Space switching: `⌥1`–`⌥5` (Option, matc
11
11
12
12
-**Bind number-row Space switches with `hs.eventtap`, not `hs.hotkey.bind`.** The keys are `⌥1`..`⌥5` (Option, matching Komorebi's `alt`+number). Two reasons the tap is needed: (1) on macOS `⌥`+number normally types special characters (`¡™£¢∞`), and the tap returns `true` to swallow the keystroke before the input system inserts the character, so the Space switches and nothing is typed (verified by reading a focused text field after a synthetic `⌥1`); (2) `⌃`+number can't go through `hs.hotkey` at all: macOS reserves `⌃1`..`⌃N` as Carbon hotkeys for "Switch to Desktop" by the number of Mission Control *user* desktops even when the System Settings checkboxes are off, so `RegisterEventHotKey` rejects them (`hs.hotkey:enable()` returns nil, console logs `-9878 ... already registered`). Diagnose hotkey issues with `hs -c 'return #hs.hotkey.getHotkeys()'` plus the Hammerspoon console.
13
13
-**`hs.spaces` reaches full-screen spaces.**`hs.spaces.spacesForScreen(uuid)` lists every Space (user and full screen/tiled, per `hs.spaces.spaceType`) in Mission Control order, and `hs.spaces.gotoSpace(id)` activates one by driving its Mission Control thumbnail, so it can land on full-screen spaces (the native `⌃`+number shortcuts cannot). Map "Space N" by indexing that list by position.
14
-
-**Window consolidation (`⌥0`) is per-screen because `hs.spaces.moveWindowToSpace` cannot cross displays.** It retargets a window's Space only on the *same* screen, so the consolidate action resolves each screen's `activeSpaceOnScreen` and collects that screen's windows onto it. `hs.window.allWindows()` does reach windows on non-focused Spaces (the AX API only enumerates the current Space, but Hammerspoon works around it — verified live: it listed a window on Space 1 while another Space was active), and `hs.spaces.windowSpaces(win)` reports a window's Space(s) so already-placed windows are skipped. Un-full-screen via `win:setFullScreen(false)` first (it collapses the app's dedicated full-screen Space back to a normal window), then wait ~1s before moving so macOS has torn the full-screen Space down. Filter to `win:isStandard()` to skip Notification Centre and other non-app windows.
14
+
-**Window consolidation (`⌥0`) is built on `removeSpace`, NOT `moveWindowToSpace`, because the latter is broken on macOS 26 (Tahoe / Darwin 25).** Verified live on 26.4: `hs.spaces.moveWindowToSpace(id, space)` returns `true` but never moves the window, for both off-Space *and* active-Space windows (so it is not a "window must be visible" constraint, it is simply non-functional). The working approach instead deletes the other Spaces: `hs.spaces.removeSpace(sp)` migrates that Space's windows onto a neighbour, so per screen, removing every `spaceType == "user"` Space except `activeSpaceOnScreen(uuid)` funnels all windows onto the one that remains (sequential `removeSpace` calls serialise fine — each drives Mission Control). Per-screen because Spaces belong to a screen.
15
+
-**`hs.window.allWindows()` only sees the *current* Space; use an all-Spaces `hs.window.filter` to reach off-Space windows.** The AX API behind `allWindows()` does not enumerate other Spaces (the original `⌥0` used it and found 0 windows because the full-screen apps were on other Spaces). `hs.window.filter.new(nil):setCurrentSpace(nil):setDefaultFilter():getWindows()`*does* enumerate every Space and reports `isFullScreen()` correctly, and `win:setFullScreen(false)` works on those off-Space windows. Consolidation un-full-screens via this filter first (collapsing each app's dedicated full-screen Space into a user Space), waits ~1s for the Spaces to tear down, then removes the leftover user Spaces.
15
16
-**Space positions are not stable while "rearrange by most recent use" is on.** With `com.apple.dock``mru-spaces` unset or true (the macOS default), visiting a full-screen space reorders it in Mission Control, so the index `⌥N` and the menu bar indicator use drifts (the order is stable only while idle). `defaults write com.apple.dock mru-spaces -bool false && killall Dock` gives stable positions; this is applied declaratively by `home/.chezmoiscripts/run_onchange_after_macos_defaults.sh.tmpl` (darwin-guarded, restarts the Dock only when it is running). The indicator (`hs.menubar` badge driven by `hs.spaces.watcher`) recomputes ~0.25s after a change so it reads the settled order, not a mid-transition one; the watcher fires before `activeSpaceOnScreen` settles otherwise.
16
17
-**Auto-reload works.**`hs.pathwatcher` uses FSEvents (path-based), so it survives chezmoi's atomic-rename writes (new inode each apply). By contrast, Finicky's `fsnotify`/kqueue watcher is inode-based and loses the watch on every chezmoi write, so Finicky needs a manual restart (see the `finicky` skill).
17
18
-**`hs.ipc` is enabled** in `init.lua`, so the running config is queryable from the `hs` CLI (`hs -c '...'`), the main way to debug Spaces and hotkeys.
0 commit comments