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