Events & Hooks
tarmac has two event systems: Lua callbacks (defined in your config) and IPC event subscriptions (for external tools). Both now carry rich payloads with detailed window and workspace metadata, designed for bar integration.
Lua event callbacks
Register callbacks in your config with gar.on(). Callbacks receive Lua tables with structured data:
gar.on("window_focused", function(info)
print("focused: " .. info.app_name .. " - " .. info.title)
end)
gar.on("workspace_changed", function(old, new)
gar.exec("sketchybar --set workspace label='" .. new .. "'")
end)
gar.on("monitor_changed", function(info)
print("monitor " .. info.index .. " workspace " .. info.focused_workspace)
end)
Available Lua events
| Event | Arguments | Description |
|---|---|---|
workspace_changed |
old (string), new (string) |
Active workspace changed |
window_focused |
info (table) |
A window gained focus |
window_created |
info (table) |
A new window appeared |
window_closed |
info (table) |
A window was closed |
layout_changed |
info (table) |
Layout recalculated on a workspace |
monitor_changed |
info (table) |
Focus moved to a different monitor |
Callback data fields
window_focused / window_created:
gar.on("window_focused", function(info)
info.window_id -- number: window ID
info.title -- string: window title
info.app_name -- string: application name
info.app_bundle -- string: bundle identifier
info.workspace -- string: workspace ID
end)
window_closed:
gar.on("window_closed", function(info)
info.window_id -- number: window ID
info.app_name -- string: application name
end)
layout_changed:
gar.on("layout_changed", function(info)
info.workspace -- string: workspace ID
info.window_count -- number: total windows
info.layout_type -- string: "tiled", "floating", or "mixed"
end)
monitor_changed:
gar.on("monitor_changed", function(info)
info.index -- number: monitor index (0-based)
info.monitor_count -- number: total monitors
info.focused_workspace -- string: workspace on this monitor
end)
Callback behavior
- Callbacks run on the main thread, so they block the event loop briefly. Keep them fast.
gar.exec()within a callback runs asynchronously (spawns a shell) and doesn't block.- JSON data from IPC events is converted to native Lua tables automatically.
- If a callback errors, the error is logged and tarmac continues.
- On config reload, all old callbacks are dropped and new ones are registered.
Use cases
Update a status bar (sketchybar)
gar.on("workspace_changed", function(old, new)
gar.exec("sketchybar --set workspace label='" .. new .. "'")
end)
Log focus with app details
gar.on("window_focused", function(info)
local msg = info.app_name .. ": " .. info.title
gar.exec("echo '" .. msg .. "' >> /tmp/tarmac-focus.log")
end)
Auto-float newly created windows from specific apps
gar.on("window_created", function(info)
if info.app_name == "Preview" then
-- Handled better via gar.rule(), but possible here
print("Preview window created: " .. info.title)
end
end)
Track layout changes
gar.on("layout_changed", function(info)
if info.layout_type == "floating" then
print("workspace " .. info.workspace .. " is all floating")
end
end)
IPC event subscriptions
For external tool integration, use IPC subscriptions. See Events & Subscribe.
All events are now available in both systems:
| Event | Lua | IPC |
|---|---|---|
workspace_changed |
yes | yes |
window_focused |
yes | yes |
window_created |
yes | yes |
window_closed |
yes | yes |
monitor_changed |
yes | yes |
layout_changed |
yes | yes |
mode_changed |
— | yes |
Combining both
You can use both systems simultaneously. Lua callbacks are convenient for quick reactions inside the config. IPC subscriptions are better for long-running external processes.
-- Quick reaction in config
gar.on("workspace_changed", function(old, new)
gar.exec("sketchybar --set workspace label='" .. new .. "'")
end)
# External process with rich data
tarmacctl subscribe workspace_changed | while read -r line; do
echo "$line" | jq '.data.workspaces[] | select(.active) | .id'
done