IPC commands
Control Shoji from the command line through Hammerspoon’s IPC mechanism. Use it for shell scripts, keyboard shortcuts from other tools, and automation.
At a glance
- Control Shoji from the shell via
hs -c - Requires
hs.ipcloaded in Hammerspoon config - Commands return
successplusdataorerror - 14 commands: status, layout control, focus, swap, and more
Prerequisites
Load the hs.ipc module in Hammerspoon config:
-- ~/.hammerspoon/init.luarequire("hs.ipc")This enables the hs CLI tool. If hs is not in PATH, run
hs.ipc.cliInstall() in the Hammerspoon console.
Basic usage
Send commands via the hs CLI:
# Get current statushs -c 'spoon.Shoji:cmd("status")'
# Cycle to next layouths -c 'spoon.Shoji:cmd("cycle-layout")'
# Set specific layout (varargs style)hs -c 'spoon.Shoji:cmd("set-layout", "wide")'
# Set specific layout (array style - also supported)hs -c 'spoon.Shoji:cmd("set-layout", {"wide"})'Type aliases
These docs use the following IPC-specific type aliases.
---@alias IPCFocusDirection---| "left"---| "right"---| "up"---| "down"---| "next"---| "prev"
---@alias IPCSwapDirection---| "next"---| "prev"---| "main"Available commands
list-commands
Returns all available command names.
hs -c 'spoon.Shoji:cmd("list-commands")'The response includes all commands: bundle-id,
cycle-layout, focus, get-layout,
list-commands, list-layouts, main-count,
ratio, reset, retile, set-layout, status,
swap, toggle-float.
status
Returns the current state of the focused space.
hs -c 'spoon.Shoji:cmd("status")'Response:
{ success = true, data = { spaceID = 1, layout = "tall", mainRatio = 0.5, mainCount = 1, windowCount = 3 }}cycle-layout
Cycles to the next layout in the enabled layouts list.
hs -c 'spoon.Shoji:cmd("cycle-layout")'Response:
{ success = true, data = { cycled = true }}set-layout
Sets a specific layout by name.
hs -c 'spoon.Shoji:cmd("set-layout", "bsp")'Arguments:
layoutName(required): Name of the layout to set
Response:
{ success = true, data = { layout = "bsp" }}Error (unknown layout):
{ success = false, error = "Unknown layout: nonexistent"}retile
Retiles the current space with current parameters. Preserves mainRatio,
mainCount, and layout state. Useful for re-applying the layout after manual
window moves.
hs -c 'spoon.Shoji:cmd("retile")'Response:
{ success = true, data = { retiled = true }}reset
Resets layout parameters (mainRatio, mainCount, layoutState) to defaults
and retiles.
hs -c 'spoon.Shoji:cmd("reset")'Response:
{ success = true, data = { reset = true }}list-layouts
Returns all available layout names.
hs -c 'spoon.Shoji:cmd("list-layouts")'get-layout
Returns the current layout for the focused space.
hs -c 'spoon.Shoji:cmd("get-layout")'bundle-id
Returns the bundle ID of the focused window. Useful
for finding bundle IDs to add to filter_apps.
hs -c 'spoon.Shoji:cmd("bundle-id")'Response:
{ success = true, data = { bundleID = "com.apple.Terminal" }}toggle-float
Toggles floating state for the focused window.
hs -c 'spoon.Shoji:cmd("toggle-float")'focus
Focus window in a direction.
# Focus directions: left, right, up, down, next, prevhs -c 'spoon.Shoji:cmd("focus", "left")'hs -c 'spoon.Shoji:cmd("focus", "next")'swap
Swap focused window with another.
# Swap directions: next, prev, mainhs -c 'spoon.Shoji:cmd("swap", "next")'hs -c 'spoon.Shoji:cmd("swap", "main")'ratio
Adjust the main area ratio.
# Increase ratio by 0.05hs -c 'spoon.Shoji:cmd("ratio", "0.05")'
# Decrease ratio by 0.1hs -c 'spoon.Shoji:cmd("ratio", "-0.1")'The ratio is clamped between 0.1 and 0.9.
main-count
Adjust the number of main windows.
# Increase main-count by 1hs -c 'spoon.Shoji:cmd("main-count", "1")'
# Decrease main-count by 1hs -c 'spoon.Shoji:cmd("main-count", "-1")'The main-count is clamped to a minimum of 1.
Response format
All commands return a table with:
---@class IPCSuccessResult---@field success true -- Whether the command succeeded---@field data table -- Result data (on success)
---@class IPCErrorResult---@field success false -- Whether the command succeeded---@field error string -- Error message (on failure)
---@alias IPCResult IPCSuccessResult|IPCErrorResultError handling
Commands fail gracefully with descriptive error messages. All error responses follow the same structure:
{ success = false, error = "error message" }Error reference
| Command | Error condition | Error message |
|---|---|---|
| (any) | Shoji not started | "Shoji is not started" |
| (any) | Unknown command | "Unknown command: <name>" |
| (any) | Handler exception | "Command failed: <message>" |
status | No focused space | "Could not get focused space" |
get-layout | No focused space | "Could not get focused space" |
set-layout | Missing argument | "Missing layout name argument" |
set-layout | Unknown layout | "Unknown layout: <name>" |
set-layout | No focused space | "Could not get focused space" |
retile | No focused space | "Could not get focused space" |
reset | No focused space | "Could not get focused space" |
focus | Missing direction | "Missing direction argument (left, right, up, down, next, prev)" |
focus | Invalid direction | "Invalid direction: <dir> (use left, right, up, down, next, prev)" |
swap | Missing direction | "Missing direction argument (next, prev, main)" |
swap | Invalid direction | "Invalid direction: <dir> (use next, prev, main)" |
ratio | Missing argument | "Missing delta argument (e.g., 0.05 or -0.05)" |
ratio | Non-numeric value | "Invalid delta: <val> (must be a number)" |
ratio | No focused space | "Could not get focused space" |
main-count | Missing argument | "Missing delta argument (e.g., 1 or -1)" |
main-count | Non-numeric value | "Invalid delta: <val> (must be a number)" |
main-count | No focused space | "Could not get focused space" |
bundle-id | No focused window | "No focused window" |
bundle-id | No bundle ID | "Could not determine bundle ID for focused window" |
Commands that operate on the focused window (focus,
swap, toggle-float) succeed silently when there are
no tiling windows — focus and swap actions no-op by
design. The error responses above are for argument
validation failures.
Scripting examples
Toggle between layouts
#!/bin/bashCURRENT=$(hs -c 'return spoon.Shoji:cmd("status").data.layout')
if [ "$CURRENT" = "tall" ]; then hs -c 'spoon.Shoji:cmd("set-layout", "wide")'else hs -c 'spoon.Shoji:cmd("set-layout", "tall")'fiGet window count
hs -c 'return spoon.Shoji:cmd("status").data.windowCount'Conditional retile
# Only retile if using BSP layoutLAYOUT=$(hs -c 'return spoon.Shoji:cmd("status").data.layout')if [ "$LAYOUT" = "bsp" ]; then hs -c 'spoon.Shoji:cmd("retile")'fitmux integration
Bind Shoji commands to tmux keys:
bind-key C-l run-shell "hs -c 'spoon.Shoji:cmd(\"cycle-layout\")'"bind-key C-t run-shell "hs -c 'spoon.Shoji:cmd(\"set-layout\", \"tall\")'"Lua API
Use the same commands from Lua:
-- In your Hammerspoon configlocal result = spoon.Shoji:cmd("status")if result.success then print("Current layout: " .. result.data.layout)endCommon mistakes
Forgetting require("hs.ipc") in init.lua:
Without hs.ipc, the hs CLI tool cannot communicate
with Hammerspoon. Add require("hs.ipc") before any
Shoji configuration.
Using print() instead of return in hs -c:
print() sends output to the Hammerspoon console, not to
stdout. Use return to get output in the terminal:
hs -c 'return spoon.Shoji:cmd("status").data.layout'.
Nested quote escaping in bash/tmux bindings:
Shell and Lua quoting interact. In tmux run-shell,
use backslash-escaped inner quotes:
run-shell "hs -c 'spoon.Shoji:cmd(\"cycle-layout\")'"
Common errors
“Shoji is not started”: Called a command before
start(). Ensure spoon.Shoji:start() runs before any
IPC commands.
“Unknown command: list-commands to see all available commands.
“Missing layout name argument”: set-layout requires
a layout name. Pass it as a second argument:
spoon.Shoji:cmd("set-layout", "tall").