Scratchpad
A scratchpad lets you toggle visibility of a window with a hotkey. Press once to show a terminal (or calculator, notes, etc.), press again to hide it. This is a common pattern in i3 and sway.
Shoji does not include a built-in scratchpad, but the extension system provides everything needed to build one.
Basic scratchpad extension
Toggle a single app by bundle ID. The window is minimized when hidden and restored when shown:
local SCRATCHPAD_APP = "com.apple.Terminal" -- change to your applocal SCRATCHPAD_KEY = { { "ctrl", "alt" }, "s" }
local scratchpad = { name = "scratchpad",
setup = function(shoji) local api = shoji.ExtensionAPI.new(shoji) local hotkey = nil
local function toggle() local app = hs.application.get(SCRATCHPAD_APP) if not app then hs.application.launchOrFocusByBundleID(SCRATCHPAD_APP) return end
local win = app:mainWindow() if not win then app:activate() return end
if win:isMinimized() then win:unminimize() win:focus() elseif win:isFocused() then win:minimize() else win:focus() end end
hotkey = hs.hotkey.bind( SCRATCHPAD_KEY[1], SCRATCHPAD_KEY[2], toggle ) scratchpad._hotkey = hotkey end,
teardown = function() if scratchpad._hotkey then scratchpad._hotkey:delete() scratchpad._hotkey = nil end end,}
spoon.Shoji:configure({ extensions = { scratchpad },})Press Ctrl+Alt+S to toggle. Change
SCRATCHPAD_APP to any bundle ID (find it with
hs.application.frontmostApplication():bundleID() in the Hammerspoon
console).
Multiple scratchpads
Support several apps, each with its own hotkey:
local scratchpads = { { app = "com.apple.Terminal", key = { { "ctrl", "alt" }, "s" } }, { app = "com.apple.Calculator", key = { { "ctrl", "alt" }, "c" } },}
local multiScratchpad = { name = "multi-scratchpad",
setup = function(_shoji) multiScratchpad._hotkeys = {}
for _, pad in ipairs(scratchpads) do local function toggle() local app = hs.application.get(pad.app) if not app then hs.application.launchOrFocusByBundleID(pad.app) return end
local win = app:mainWindow() if not win then app:activate() return end
if win:isMinimized() then win:unminimize() win:focus() elseif win:isFocused() then win:minimize() else win:focus() end end
local hk = hs.hotkey.bind(pad.key[1], pad.key[2], toggle) table.insert(multiScratchpad._hotkeys, hk) end end,
teardown = function() for _, hk in ipairs(multiScratchpad._hotkeys or {}) do hk:delete() end multiScratchpad._hotkeys = nil end,}
spoon.Shoji:configure({ extensions = { multiScratchpad },})Auto-float scratchpad windows
Scratchpad windows often work better as floating so they overlay the
tiled layout. Combine with the window_created hook to auto-float:
local SCRATCHPAD_APPS = { ["com.apple.Terminal"] = true, ["com.apple.Calculator"] = true,}
local autoFloat = { name = "scratchpad-auto-float",
setup = function(shoji) local api = shoji.ExtensionAPI.new(shoji)
autoFloat._removeHook = api.registerHook( "window_created", function(windowID) local win = hs.window.find(windowID) if not win then return end
local app = win:application() if not app then return end
if SCRATCHPAD_APPS[app:bundleID()] then shoji.state:setFloating(windowID, true) end end ) end,
teardown = function() if type(autoFloat._removeHook) == "function" then autoFloat._removeHook() end end,}Combine both extensions:
spoon.Shoji:configure({ extensions = { multiScratchpad, autoFloat },})How it works
The scratchpad uses standard Hammerspoon window APIs:
hs.window:minimize()hides the window to the Dockhs.window:unminimize()restores iths.window:focus()brings it to front
Minimized windows are removed from Shoji’s tiling layout automatically (Shoji treats minimized windows as hidden). When unminimized, they re-enter the layout unless marked as floating.