The widget wire API is for live publishers that don't fit the YAML polling model — long-running daemons, Mac apps, build watchers, anything that pushes when state changes. Two transports speak the same JSON shape:
- Unix socket at
~/Library/Application Support/ApexDock/api/widgets.sock. Per-connection ownership: closing the socket drops every widget that connection published. - File drop — drop a
*.jsonfile in~/Library/Application Support/ApexDock/widgets/. Picked up via FSEvents, reloaded on change. Filename basename is the defaultid.
For polling-based widgets (CPU%, weather, clock), the YAML transport is simpler — it owns the polling loop, the hot-reloader, and the rich rendering tree. YAML files also live in the same widgets/ folder, but as one .yaml / .yml file per widget. Reach for the wire API when you need a push model.
Schema
A single widget has the same payload over both transports:
{
"id": "cpu-load",
"label": "37%",
"symbol": "cpu",
"iconPath": null,
"tint": "#FF8800",
"tooltip": "CPU load (1m)",
"click": { "type": "shell", "command": "open -a 'Activity Monitor'" },
"order": 100
}
| Field | Type | Notes |
|---|---|---|
id | string (required) | Stable widget identifier. Unique per publisher. |
label | string | Short text chip. Truncated to 8 chars. |
symbol | string | SF Symbol name. Wins over iconPath. |
iconPath | string | Filesystem path or data:image/...;base64,.... |
tint | string | #RRGGBB, #RRGGBBAA, or named SwiftUI color. Applied to SF Symbols only. |
tooltip | string | Hover text. Defaults to id. |
click | object | One of none/shell/url/palette. |
order | int | Sort key, lower = leftmost. Ties broken by id. Default 0. |
Falls back to circle.fill if neither symbol nor a readable iconPath is provided.
Click actions
{ "type": "none" }
{ "type": "shell", "command": "open -a 'Activity Monitor'" }
{ "type": "url", "url": "https://github.com/notifications" }
{ "type": "palette", "query": "switch to Work" }
- shell — runs via
/bin/sh -c <command>, detached. Hard-killed after 5s. - url — handed to
NSWorkspace.shared.open(url). - palette — fuzzy-matches the query against the command palette index and runs the best match.
Limits
- Max visible widgets per zone: 32. Sorted by
order, thenid; the tail is dropped with a console warning. labelis truncated to 8 characters before render.
Push protocol (socket)
Each line is one frame:
t | Fields | Effect |
|---|---|---|
upsert | v: 1, id, plus any other widget fields | Creates or replaces by id |
remove | v: 1, id | Removes a widget by id |
clear | v: 1 | Removes all widgets pushed by this connection |
Server response per frame: {"ok": true} on success, {"ok": false, "error": "..."} on bad input.
When a connection closes (intentional or not), every widget it published is dropped. No risk of one client wiping another's widgets, no leftover state from crashed publishers.
File-drop protocol
Any direct child *.json file in ~/Library/Application Support/ApexDock/widgets/ is treated as a single JSON wire widget. The filename basename (sans .json) is the default id if the JSON omits it. Edits picked up via FSEvents with a 100ms debounce. Files ending in .yaml or .yml in the same folder are handled by the YAML widget runner instead.
File-drop widgets persist across launches. Remove the file to remove the widget. Same-id collisions: a socket-pushed widget shadows a file widget.
CLI
The bundled apexdock widget subcommand picks the transport automatically (socket if present, file-drop otherwise) and handles JSON construction with proper escaping.
apexdock widget upsert --id cpu-load --symbol cpu --label "37%" --tint "#FF8800" \
--tooltip "CPU load (1m)" --click-shell "open -a 'Activity Monitor'" --order 100
apexdock widget remove --id cpu-load
apexdock widget clear
apexdock widget list
At most one --click-* flag per upsert. Run apexdock widget --help (or apexdock widget upsert --help) for the full flag set.
Smoke tests
# Socket — push and remove a widget
printf '{"v":1,"t":"upsert","id":"smoke","symbol":"bolt","tooltip":"hello"}\n' \
| socat - UNIX:"$HOME/Library/Application Support/ApexDock/api/widgets.sock"
# File-drop — same effect
mkdir -p "$HOME/Library/Application Support/ApexDock/widgets"
cat > "$HOME/Library/Application Support/ApexDock/widgets/smoke.json" <<'EOF'
{"symbol": "bolt", "tooltip": "hello"}
EOF
# Tear down
rm "$HOME/Library/Application Support/ApexDock/widgets/smoke.json"
Push examples
CPU load (socket loop)
#!/usr/bin/env bash
while true; do
pct=$(ps -A -o %cpu | awk '{s+=$1} END {printf "%.0f", s/'"$(sysctl -n hw.ncpu)"'}')
apexdock widget upsert --id cpu-load \
--symbol cpu --label "${pct}%" \
--tint "#FF8800" --tooltip "CPU load" \
--click-shell "open -a 'Activity Monitor'" --order 100
sleep 10
done
GitHub notifications
#!/usr/bin/env bash
while true; do
n=$(gh api notifications --jq 'length' 2>/dev/null || echo 0)
apexdock widget upsert --id gh-notifs \
--symbol "bell.fill" --label "$n" \
--tint "$([ "$n" -gt 0 ] && echo '#FF3B30' || echo 'gray')" \
--tooltip "GitHub notifications" \
--click-url "https://github.com/notifications" --order 200
sleep 60
done
Build status (filesystem watcher)
#!/usr/bin/env bash
inotifywait --monitor --event modify ./build-state.txt | while read -r _ _ file; do
state=$(cat ./build-state.txt)
case "$state" in
pass) apexdock widget upsert --id build --symbol "checkmark.circle.fill" --tint green ;;
fail) apexdock widget upsert --id build --symbol "xmark.circle.fill" --tint red ;;
pending) apexdock widget upsert --id build --symbol "clock.fill" --tint orange ;;
esac
done
Limitations (v1)
- SF-Symbol tints aren't applied to bitmap
iconPathimages. - Schema is
v: 1. Future breaking changes will bump tov: 2. - Wire widgets only support the legacy single-icon-with-label model. For rich SwiftUI trees with hover popovers, use the YAML transport.