Debug: Hammerspoon Workspace Search
Debugging the Hammerspoon workspace search script β data model, known issues, and console log audit guide.
How It Works
The workspace search lives in ~/.hammerspoon/init.lua. Two hotkeys:
CMD+SHIFT+Sβ saves the active space on every connected screen under a nameCMD+SHIFT+SPACEβ search saved workspaces, select one to switch all screens to their saved spaces
Data Model
Workspaces are stored in hs.settings under the key "workspaces". Each entry looks like:
{
"name": "reading",
"spaces": {
"SCREEN-UUID-1": 42,
"SCREEN-UUID-2": 57
}
}The spaces table maps each screen's UUID to the macOS space ID that was active on that screen when saved. On switch, the script calls hs.spaces.gotoSpace(spaceID) for each entry.
Known Failure Modes
1. Space IDs Are Ephemeral
macOS assigns integer space IDs at boot. They change on restart, and can change when spaces are added/removed/reordered. A workspace saved yesterday may have stale IDs today. The script detects this by checking against hs.spaces.allSpaces() and marks stale entries in the chooser.
2. Screen UUIDs Can Change
Screen UUIDs come from screen:getUUID(). If a monitor is unplugged and re-plugged, or display arrangement changes, the UUID may differ. The script would still call gotoSpace with the right space ID, but the UUID key in the saved data won't match the current screen UUID β meaning the lookup structure is stale even if the space IDs happen to still be valid.
3. gotoSpace Only Switches the Screen That Owns the Space
With "Displays have separate Spaces" enabled, each screen has its own set of space IDs. Calling hs.spaces.gotoSpace(42) only switches the screen that owns space 42. If both screens need to switch, we need two calls β one per screen's saved space ID. The original single-spaceID version only saved one screen's space.
4. gotoSpace May Silently Fail
hs.spaces.gotoSpace() can return success but not actually switch. This happens when: the space exists but macOS is mid-animation, the space belongs to a fullscreen app, or System Integrity Protection blocks the private API. The debug logging prints the pcall result so we can see if the call itself errors.
Console Log Guide
Open the Hammerspoon console (CMD+SHIFT+SPACE on the Hammerspoon menu bar icon, or hs.openConsole()). All workspace operations now print [WS]-prefixed logs.
On Save (CMD+SHIFT+S)
[WS] Screen: LG HDR 4K (UUID=ABC-123) β spaceID=42
[WS] Screen: Built-in Retina (UUID=DEF-456) β spaceID=57
[WS] Loaded 3 workspaces from settings
[WS] 1. jaygriff β ABC-123=38 DEF-456=51
[WS] 2. reading β ABC-123=42 DEF-456=57
[WS] 3. finance β ABC-123=45 DEF-456=60
[WS] Saving 3 workspaces
[WS] Saved workspace 'reading' with spaces:
[WS] ABC-123 β 42
[WS] DEF-456 β 57On Switch (CMD+SHIFT+SPACE β select)
[WS] Loaded 3 workspaces from settings
[WS] 1. jaygriff β ABC-123=38 DEF-456=51
[WS] 2. reading β ABC-123=42 DEF-456=57
[WS] 3. finance β ABC-123=45 DEF-456=60
[WS] Screen: LG HDR 4K (UUID=ABC-123) β spaceID=38
[WS] Screen: Built-in Retina (UUID=DEF-456) β spaceID=51
[WS] Switching to workspace: reading
[WS] gotoSpace(42) for screen ABC-123
[WS] result: ok=true err=nil
[WS] gotoSpace(57) for screen DEF-456
[WS] result: ok=true err=nilWhat to Check
- Are both screens detected? Look for two
[WS] Screen:lines on save. If only one, Hammerspoon isn't seeing the second display. - Are both space IDs being saved? The
Saved workspacelog should show two UUID β spaceID entries. - Are both gotoSpace calls firing? On switch, there should be two
gotoSpace(...)lines. If only one, the spaces table is incomplete. - Do the space IDs match? Compare the saved IDs (from load log) with current screen IDs (from
[WS] Screen:lines). If they differ, the spaces changed since save. - Does gotoSpace return ok=true but nothing happens? That's macOS silently ignoring the call β likely an animation conflict or SIP issue.
Quick Hammerspoon Console Commands
-- Dump all current space IDs per screen
for uuid, spaces in pairs(hs.spaces.allSpaces()) do print(uuid, hs.inspect(spaces)) end
-- Check what's saved
hs.inspect(hs.settings.get("workspaces"))
-- Nuke all saved workspaces and start fresh
hs.settings.set("workspaces", {})
-- Manually try switching to a specific space ID
hs.spaces.gotoSpace(42)