This page builds one widget end-to-end. By the end you'll have:
- A live battery percentage in the bar
- A red ⚠️ chip that appears only when battery is below 20%
- A hover popover showing the full charge state, ETA, and a colour gauge
Open the widget folder
mkdir -p "$HOME/Library/Application Support/ApexDock/widgets"
$EDITOR "$HOME/Library/Application Support/ApexDock/widgets/battery.yml"
Or use Settings → Integrations → YAML Widgets → Open and create battery.yml there.
Step 1: a label-only widget
id: battery
command: |
pmset -g batt | awk '/InternalBattery/ { match($0, /[0-9]+%/); print substr($0, RSTART, RLENGTH); }'
interval: 30
symbol: battery.100
tooltip: "Battery"
order: 50
Save. Within ~2 seconds the bar shows a battery glyph with the current percentage. The script's first line of stdout becomes the label.
Step 2: switch to JSON output
The label-only mode is fine, but we need access to the percentage as a number for the gauge. Switch to JSON:
id: battery
command: |
pmset -g batt | awk '
/InternalBattery/ {
match($0, /[0-9]+%/); pct = substr($0, RSTART, RLENGTH-1)
match($0, /[0-9]+:[0-9]+/); rem = (RSTART > 0) ? substr($0, RSTART, RLENGTH) : ""
charging = ($0 ~ /; charging/) ? "true" : "false"
printf "{\"pct\":%s,\"charging\":%s,\"remaining\":\"%s\"}\n", pct, charging, rem
}'
interval: 30
...
stdout is now a single JSON object. The runner parses it and exposes the keys to bindings.
Step 3: add a rich view
Replace the symbol/tooltip keys with a view: tree:
view:
hstack:
spacing: 4
children:
- symbol: { name: battery.100, color: green }
- text:
content: "${pct}%"
size: 10
weight: medium
design: rounded
monospacedDigit: true
Save. The bar now renders a green battery glyph + the percentage number, with smooth animation as the value changes.
Step 4: change colour when low
Wrap the symbol in an if:
view:
hstack:
spacing: 4
children:
- if:
when: "${pct} < 20"
then:
symbol: { name: battery.0, color: "#FF3B30" }
else:
symbol: { name: battery.100, color: green }
- text: { content: "${pct}%", size: 10, weight: medium, design: rounded, monospacedDigit: true }
The bar now flips to a red empty-battery glyph below 20%.
Step 5: add a hover popover
Add a hover: tree at the top level (sibling of view:):
hover:
vstack:
spacing: 8
alignment: leading
frame: { width: 200 }
children:
- hstack:
children:
- text: { content: "Battery", size: 11, color: secondary, weight: medium }
- spacer: {}
- text: { content: "${pct}%", size: 18, weight: semibold, design: rounded }
- gauge:
value: "${pct}"
min: 0
max: 100
tint: green
width: 176
height: 6
- if:
when: "${charging} == true"
then:
text: { content: "Charging — ${remaining} until full", size: 11, color: secondary }
else:
text: { content: "${remaining} remaining", size: 11, color: secondary }
Hover the bar cell. A 200-pt-wide popover slides up with the full state.
Step 6: open System Settings on click
Add click: at the top level:
click:
shell: "open 'x-apple.systempreferences:com.apple.preference.battery'"
Now clicking the cell opens System Settings → Battery.
You shipped a widget
Total time: ~5 minutes. The complete file is in Recipes → Battery. From here:
- Read Schema for the full set of nodes and modifiers
- Read Bindings for path resolution and format specs
- Read Control flow for
forEach(lists),if(branches), andshowIf(visibility) - Read Recipes for full-featured examples — disk, weather, GitHub PRs
Debugging tips
- Save and watch the bar. YAML reload is FSEvent-driven; you'll see changes within ~100ms.
- Parse errors show inline. A bad YAML emits a red ⚠️ chip in the tray with the error in its tooltip. Click the chip to reopen the file.
- Check
os_log.log show --predicate 'subsystem == "apexdock.widgets"' --info --last 5mfor the full log, including widget warnings. - Test the script standalone. Run your
commandvalue in a shell and check stdout is single-line JSON.