Skip to content

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.

  • Control Shoji from the shell via hs -c
  • Requires hs.ipc loaded in Hammerspoon config
  • Commands return success plus data or error
  • 14 commands: status, layout control, focus, swap, and more

Load the hs.ipc module in Hammerspoon config:

-- ~/.hammerspoon/init.lua
require("hs.ipc")

This enables the hs CLI tool. If hs is not in PATH, run hs.ipc.cliInstall() in the Hammerspoon console.

Send commands via the hs CLI:

Terminal window
# Get current status
hs -c 'spoon.Shoji:cmd("status")'
# Cycle to next layout
hs -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"})'

These docs use the following IPC-specific type aliases.

---@alias IPCFocusDirection
---| "left"
---| "right"
---| "up"
---| "down"
---| "next"
---| "prev"
---@alias IPCSwapDirection
---| "next"
---| "prev"
---| "main"

Returns all available command names.

Terminal window
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.

Returns the current state of the focused space.

Terminal window
hs -c 'spoon.Shoji:cmd("status")'

Response:

{
success = true,
data = {
spaceID = 1,
layout = "tall",
mainRatio = 0.5,
mainCount = 1,
windowCount = 3
}
}

Cycles to the next layout in the enabled layouts list.

Terminal window
hs -c 'spoon.Shoji:cmd("cycle-layout")'

Response:

{
success = true,
data = { cycled = true }
}

Sets a specific layout by name.

Terminal window
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"
}

Retiles the current space with current parameters. Preserves mainRatio, mainCount, and layout state. Useful for re-applying the layout after manual window moves.

Terminal window
hs -c 'spoon.Shoji:cmd("retile")'

Response:

{
success = true,
data = { retiled = true }
}

Resets layout parameters (mainRatio, mainCount, layoutState) to defaults and retiles.

Terminal window
hs -c 'spoon.Shoji:cmd("reset")'

Response:

{
success = true,
data = { reset = true }
}

Returns all available layout names.

Terminal window
hs -c 'spoon.Shoji:cmd("list-layouts")'

Returns the current layout for the focused space.

Terminal window
hs -c 'spoon.Shoji:cmd("get-layout")'

Returns the bundle ID of the focused window. Useful for finding bundle IDs to add to filter_apps.

Terminal window
hs -c 'spoon.Shoji:cmd("bundle-id")'

Response:

{
success = true,
data = { bundleID = "com.apple.Terminal" }
}

Toggles floating state for the focused window.

Terminal window
hs -c 'spoon.Shoji:cmd("toggle-float")'

Focus window in a direction.

Terminal window
# Focus directions: left, right, up, down, next, prev
hs -c 'spoon.Shoji:cmd("focus", "left")'
hs -c 'spoon.Shoji:cmd("focus", "next")'

Swap focused window with another.

Terminal window
# Swap directions: next, prev, main
hs -c 'spoon.Shoji:cmd("swap", "next")'
hs -c 'spoon.Shoji:cmd("swap", "main")'

Adjust the main area ratio.

Terminal window
# Increase ratio by 0.05
hs -c 'spoon.Shoji:cmd("ratio", "0.05")'
# Decrease ratio by 0.1
hs -c 'spoon.Shoji:cmd("ratio", "-0.1")'

The ratio is clamped between 0.1 and 0.9.

Adjust the number of main windows.

Terminal window
# Increase main-count by 1
hs -c 'spoon.Shoji:cmd("main-count", "1")'
# Decrease main-count by 1
hs -c 'spoon.Shoji:cmd("main-count", "-1")'

The main-count is clamped to a minimum of 1.

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|IPCErrorResult

Commands fail gracefully with descriptive error messages. All error responses follow the same structure:

{ success = false, error = "error message" }
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.

#!/bin/bash
CURRENT=$(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")'
fi
Terminal window
hs -c 'return spoon.Shoji:cmd("status").data.windowCount'
Terminal window
# Only retile if using BSP layout
LAYOUT=$(hs -c 'return spoon.Shoji:cmd("status").data.layout')
if [ "$LAYOUT" = "bsp" ]; then
hs -c 'spoon.Shoji:cmd("retile")'
fi

Bind Shoji commands to tmux keys:

~/.tmux.conf
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\")'"

Use the same commands from Lua:

-- In your Hammerspoon config
local result = spoon.Shoji:cmd("status")
if result.success then
print("Current layout: " .. result.data.layout)
end

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\")'"

“Shoji is not started”: Called a command before start(). Ensure spoon.Shoji:start() runs before any IPC commands.

“Unknown command: ”: Typo in the command name. Run 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").