API

Widget wire (socket + file drop)

Push tray widgets from any process via Unix socket NDJSON or filesystem drop.

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 *.json file in ~/Library/Application Support/ApexDock/widgets/. Picked up via FSEvents, reloaded on change. Filename basename is the default id.

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:

json
{
  "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
}
FieldTypeNotes
idstring (required)Stable widget identifier. Unique per publisher.
labelstringShort text chip. Truncated to 8 chars.
symbolstringSF Symbol name. Wins over iconPath.
iconPathstringFilesystem path or data:image/...;base64,....
tintstring#RRGGBB, #RRGGBBAA, or named SwiftUI color. Applied to SF Symbols only.
tooltipstringHover text. Defaults to id.
clickobjectOne of none/shell/url/palette.
orderintSort 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

json
{ "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, then id; the tail is dropped with a console warning.
  • label is truncated to 8 characters before render.

Push protocol (socket)

Each line is one frame:

tFieldsEffect
upsertv: 1, id, plus any other widget fieldsCreates or replaces by id
removev: 1, idRemoves a widget by id
clearv: 1Removes 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.

bash
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

bash
# 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)

bash
#!/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

bash
#!/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)

bash
#!/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 iconPath images.
  • Schema is v: 1. Future breaking changes will bump to v: 2.
  • Wire widgets only support the legacy single-icon-with-label model. For rich SwiftUI trees with hover popovers, use the YAML transport.