Skip to content

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 app
local 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 Dock
  • hs.window:unminimize() restores it
  • hs.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.