The widget YAML schema is a recursive tree of nodes and modifiers, decoded by the runner and rendered through SwiftUI at runtime. Each *.yaml or *.yml file in ~/Library/Application Support/ApexDock/widgets/ is one widget.
Top-level entry
id: <string> # optional; defaults to filename without .yaml/.yml
command: <string> # required, shell command (/bin/sh -c)
interval: <seconds> # default 60, clamped to >= 1
order: <int> # default 0, lower = leftmost
enabled: <bool> # default true
location: <string> # tray | pinned | unpinned | agent | leading | trailing
tooltip: <string> # cell hover text
click: # cell click action (one of)
shell: <string>
url: <string>
palette: <string>
symbol: <string> # SF Symbol — legacy mode (ignored when `view` is set)
icon: <string> # path / data: URL — legacy mode
tint: <string> # legacy mode
label: <string> # legacy mode
output: label | json # legacy mode (ignored when `view` is set)
history: # ring buffers exposed under ${history.<field>}
<field>: <int> # capacity (samples)
view: <Node> # rich rendering — see Nodes below
hover: <Node> # popover content; rendered with no size cap
When view is set, symbol/icon/tint/label/output are ignored. The script's stdout is parsed as JSON and bound into the tree.
Nodes
A node is a YAML mapping with exactly one of the following kind keys, plus any number of modifier siblings.
Layout
| Kind | Spec |
|---|---|
hstack | { children, spacing, alignment } — horizontal stack |
vstack | { children, spacing, alignment } — vertical stack |
zstack | { children, spacing, alignment } — depth stack (overlay) |
spacer | { minLength } — flexible space |
divider | (no spec) — horizontal separator |
Stack alignment values:
- hstack:
top,center(default),bottom,firstTextBaseline,lastTextBaseline - vstack:
leading,center(default),trailing - zstack:
topLeading,top,topTrailing,leading,center(default),trailing,bottomLeading,bottom,bottomTrailing
Display primitives
| Kind | Spec |
|---|---|
text | { content, size, weight, design, color, lineLimit, monospacedDigit } |
symbol | { name, size, weight, color, renderingMode } |
image | { path, width, height } |
gauge | { value, min, max, tint, style, width, height } |
progress | { value, min, max, tint, background, width, height, cornerRadius } |
sparkline | { values, min, max, tint, width, height, lineWidth, fillOpacity } |
shape | { kind, fill, stroke, lineWidth, width, height, cornerRadius } |
Control flow
| Kind | Spec |
|---|---|
forEach | { in, template, stack, spacing, alignment, limit } — iterate an array |
if | { when, then, else } — branch on condition |
See Control flow for details.
Modifiers
Modifiers apply to the node they're declared on. All optional, all sibling fields of the kind key:
| Field | Type | Effect |
|---|---|---|
padding | { all?, horizontal?, vertical?, top?, bottom?, leading?, trailing? } | Edge insets in pt |
background | string | Color (#RRGGBB, named) or material (regular, thin, ultraThin) |
cornerRadius | number | Corner radius in pt |
frame | { width?, height?, minWidth?, minHeight?, maxWidth?, maxHeight?, alignment? } | Fixed/min/max size |
color | string | Foreground color (applies via foregroundStyle) |
tooltip | string | Native macOS hover help; supports ${} interpolation |
opacity | number | 0–1 |
onTap | { shell|url|palette } | Click action; URL supports ${} interpolation |
showIf | string | Visibility predicate (same grammar as if.when) |
Spec field details
TextSpec
| Field | Type | Notes |
|---|---|---|
content | string (required) | Supports ${} interpolation. Multi-binding strings welcome. |
size | number | pt |
weight | string | ultralight, thin, light, regular, medium, semibold, bold, heavy, black |
design | string | default, rounded, monospaced, serif |
color | string | #hex or named |
lineLimit | int | Defaults to 1 |
monospacedDigit | bool | Forces tabular figures so digits don't jitter |
SymbolSpec
| Field | Type | Notes |
|---|---|---|
name | string (required) | SF Symbol name. Supports ${} interpolation. |
size | number | pt; default 14 |
weight | string | Same set as text |
color | string | #hex or named |
renderingMode | string | monochrome, hierarchical, palette, multicolor |
ImageSpec
| Field | Type | Notes |
|---|---|---|
path | string (required) | Filesystem path or data:image/...;base64,.... Supports ${} interpolation. |
width, height | number | pt |
GaugeSpec
| Field | Type | Notes |
|---|---|---|
value | bindable number (required) | The current value |
min | bindable number | Default 0 |
max | bindable number | Default 100 |
tint | string | #hex or named; default accent |
style | string | linear (default) or circular |
width, height | number | Linear default 40×6; circular default 18×18 |
SparklineSpec
| Field | Type | Notes |
|---|---|---|
values | bindable array (required) | [Double] or a binding to one |
min, max | bindable number | Optional fixed range; falls back to sample min/max |
tint | string | Default accent |
width | number | Default 40 |
height | number | Default 18 |
lineWidth | number | Default 1 |
fillOpacity | number | Default 0.18 |
ProgressSpec
| Field | Type | Notes |
|---|---|---|
value | bindable number (required) | The current value |
min | bindable number | Default 0 |
max | bindable number | Default 100 |
tint | string | Filled portion color; default accent |
background | string | Track color; default low-opacity primary |
width, height | number | Default 40×6 |
cornerRadius | number | Default half the rendered height |
ShapeSpec
| Field | Type | Notes |
|---|---|---|
kind | string | roundedRectangle (default), rectangle, circle, or capsule |
fill | string | Fill color; default low-opacity primary |
stroke | string | Optional stroke color |
lineWidth | number | Default 1 |
width, height | number | Defaults depend on shape |
cornerRadius | number | Rounded rectangle corner radius; default 4 |
ForEachSpec
| Field | Type | Notes |
|---|---|---|
in | string (required) | Binding to an array, e.g. "${prs}" |
template | Node (required) | Rendered once per item; binds ${item} and ${index} |
stack | string | vstack (default), hstack, zstack |
spacing | number | Stack spacing |
alignment | string | Stack alignment |
limit | int | Cap; useful when source can grow |
IfSpec
| Field | Type | Notes |
|---|---|---|
when | string (required) | Expression — see Control flow |
then | Node (required) | Rendered when when is truthy |
else | Node | Rendered when when is false; absent = render nothing |
Bindable values
Numeric and array fields accept either a literal or a binding:
# Literal
value: 42
values: [1, 2, 3, 4, 5]
# Binding
value: "${pct}"
values: "${history.pct}"
Strings always pass through binding interpolation: "${city}, ${temp_f}°F" → "SF, 68°F".
Color values
#RRGGBB, #RRGGBBAA, or a named color: red, orange, yellow, green, mint, teal, cyan, blue, indigo, purple, pink, brown, gray, black, white, primary, secondary, accent.
Backgrounds also accept material values: regular, thin, ultraThin.
Click actions
Both top-level click: (cell-wide) and node-level onTap: accept the same shape:
click:
shell: "open -a 'Activity Monitor'"
# or
click:
url: "https://example.com"
# or
click:
palette: "switch to Work"
Inside onTap:, all three fields support ${} interpolation, so url: "${item.html_url}" works for clickable list rows.
Output modes (legacy view-less widgets)
When view: is not set, the widget falls back to the legacy single-icon-with-label model:
output | Behavior |
|---|---|
label (default) | First line of stdout becomes the chip label, truncated to 8 chars |
json | stdout is parsed as a full widget descriptor — same shape as a file-drop JSON |
These modes are useful when you don't need a rich tree. Most new widgets should use view:.
Limits
- 32 widgets visible per zone (sorted by
order, thenid; tail dropped with a console warning). - 5-second wall-clock kill on each
commandtick. - Single-process per tick — overlapping ticks are dropped.
- 8-character chip label cap (legacy mode).
Reload semantics
The runner watches ~/Library/Application Support/ApexDock/widgets/ via FSEvents and reloads direct child *.yaml / *.yml files on save (100ms debounce). YAML files outside that folder, hidden files, nested files, and the old parent widgets.yaml file are ignored. Diffing is by id:
- New id → schedule + immediate first tick
- Changed config for same id → restart with new config (preserves history rings if
history:matches) - Removed id → remove from store, kill any in-flight subprocess
A YAML parse error keeps that file's previous valid widget running, leaves other widget files alone, and emits a red warning chip with the filename and parse error in its tooltip.