KiwiDesk never triggers sketchybar events itself. Instead, you
write a hook in your init.lua that listens to KiwiDesk events
and calls sketchybar --trigger <custom-event> via
KiwiDesk.exec. Your sketchybar items subscribe to that custom
event and update when it fires.
This decouples the two processes: sketchybar’s script runs in
its own Lua sandbox, outside the app watchdog and main-thread
restrictions.
Wedged daemon → runaway triggers.sketchybar --trigger normally exits in milliseconds. If the
sketchybar daemon is absent or wedged, the trigger blocks instead
— and because these hooks fire on every KiwiDesk event,
unbounded fire-and-forget would let thousands of stuck --trigger
processes pile up (re-parented to launchd, some alive for hours),
measurably loading the scheduler.
KiwiDesk.exec bounds this automatically: each trigger gets a
30 s default timeout and identical in-flight commands are
deduped, so even a permanently wedged daemon leaves at most
one stuck child per distinct trigger, reaped every 30 s (see
KiwiDesk.exec). KiwiDesk
also warns in its log once more than 20 children are outstanding.
If you hit a pile-up from an older config (or a hard-wedged
daemon), clear the clients and restart the daemon:
Terminal window
pkill-f"sketchybar --trigger"# clients only, not the daemon
All nine events fire whenever KiwiDesk’s state changes. Hook
any of them to keep sketchybar in sync:
Event
Lua arguments
Fires when
space_change
space_id, mode
Virtual space switches
layout_change
space_id, mode
Layout mode changes on a space
focus_change
window_id, app, bundle_id
Focused window changes
monitor_change
monitor_count
Monitors connect or disconnect
native_space_change
native_space
The native macOS desktop switches
window_created
window_id, app, space, reason, bundle_id
A managed window appears (new/returned/restored)
window_destroyed
window_id, app, space, reason, bundle_id
A managed window disappears (closed/minimized/vanished)
window_moved_to_space
window_id, app, from, to, bundle_id
See caveats
Caveats:
window_created and window_destroyed track the visible
window set, not app lifecycle — deminiaturizing and native
Space switches also fire them. Filter on the reason
argument to ignore the artifacts (vanished/returned
bursts on every Mission Control round-trip). If you do
filter, keep a native_space_change handler that re-queries
state: a window closed while its desktop was off-screen was
already emitted as vanished and never gets a corrective
closed.
space in Lua callbacks is always a string (the space id).
A window on an unknown space receives "" (empty string
instead of nil, so the argument list doesn’t truncate). In
the CLI JSON event stream, the key is space_id and the
value is null for unknown spaces.
window_moved_to_space fires only on explicit
move_to_space commands with a different target;
bulk operations like profile loads stay silent.
The widget creates one item per space, pre-allocates window
icon slots beneath each, and polls state via
kiwidesk get_state | jq on every kiwidesk_update event. A
cold-start watchdog polls until the first successful read, then
goes event-driven.
-- native_space_change is not optional: a window closed while
-- its desktop was off-screen never gets a corrective event,
-- so the Mission Control round-trip must re-query (see the
-- caveats above).
for_, eventinipairs({
"space_change",
"layout_change",
"focus_change",
"native_space_change",
"window_created",
"window_destroyed",
"window_moved_to_space",
}) do
KiwiDesk.on(event, function()
KiwiDesk.exec(
"sketchybar --trigger kiwidesk_update"
)
end)
end
Keep your hooks in init.lua under GUI-managed settings
When the Settings app takes ownership of your config
(adopting it into gui.json), only the managed parts of
init.lua go inert — tiling settings, keybindings, window
rules. KiwiDesk.on(...) event hooks are not managed: they
keep running from init.lua on every reload, and init.lua
stays the only place they can live. Don’t delete your
sketchybar hooks because “the GUI owns the config now” —
that silently kills the bar.
If sketchybar loads items conditionally (e.g., only when
KiwiDesk is running) and its item loader fires before
KiwiDesk’s config process reaches the event loop, the initial
update_spaces() call in the item script will fail (the socket
isn’t ready). Add a grace period to your loader:
Terminal window
sleep1 && sketchybar--additemkiwidesk_spacesright
Or use the watchdog above, which polls until the first
successful read.
io.popen inside sketchybar’s SbarLua
The sketchybar widget script runs in sketchybar’s own Lua
process, not inside KiwiDesk’s. sketchybar bundles its own
Lua (SbarLua), separate from KiwiDesk’s embedded 5.5 VM, and
it is a normal unrestricted runtime — so io.popen is fully
available. Inside KiwiDesk’s Lua VM, io.popen is disabled
to prevent blocking the main thread — use KiwiDesk.exec
with a callback instead (see Lua
reference).