Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ workspace tokens.

Renders in the left sidebar, per workspace:

- GPU % (Apple Silicon, rootless — bundled `gpu-util` helper reads the IORegistry via public IOKit user-space APIs; no sudo, no powermetrics; `--` without the stat)
- CPU % (100 − idle)
- Memory — active + wired pages → GB (port of `~/dotfiles/tmux/memory.sh`)
- Battery — Nerd Font glyph + percentage, charging-aware
Expand Down Expand Up @@ -66,6 +67,7 @@ correct on your system.

- Herdr ≥ 0.7.0
- macOS (v0.1 uses `pmset`, `vm_stat`, `df`). Linux reads land in v0.2.
- `gpu-util` rebuilds with `clang` when available; otherwise the bundled arm64 binary is used.
- `jq` (for parsing `herdr … --json`)
- A Nerd Font for the battery / wifi / clock glyphs (JetBrainsMono Nerd Font
works, matching the existing Ghostty config)
Expand Down
Binary file added bin/gpu-util
Binary file not shown.
74 changes: 74 additions & 0 deletions bin/gpu-util.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* gpu-util — rootless GPU utilization for Apple Silicon.
*
* Reads the GPU's "Device Utilization %" out of the IORegistry
* (PerformanceStatistics on the AGX accelerator service). Pure user-space
* IOKit access — no sudo, no powermetrics, no private framework linkage
* (the symbols used are all public IOKit/CoreFoundation APIs).
*
* Usage: gpu-util prints an integer percent (e.g. "3")
* prints "--" when no GPU/stat is found.
* gpu-util --raw same, but exits 1 when unavailable
*
* The chip-generation-specific class name (AGXAcceleratorG14X, G13, …)
* is deliberately NOT matched by name; the recursive property search finds
* whatever accelerator the current SoC exposes, so the helper survives
* future GPU generations without changes.
*
* Build (macOS, Xcode CLT):
* clang -O2 -framework IOKit -framework CoreFoundation \
* -o gpu-util gpu-util.c
*/

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <stdio.h>
#include <stdlib.h>

/* The utilization value lives NESTED inside the accelerator's
* "PerformanceStatistics" dictionary, so the search must first locate
* that top-level property, then read the sub-key from the dict. */
#define STATS_KEY CFSTR("PerformanceStatistics")
#define UTIL_KEY CFSTR("Device Utilization %")

int main(int argc, char *argv[]) {
int raw = (argc > 1 && strcmp(argv[1], "--raw") == 0);

io_registry_entry_t root = MACH_PORT_NULL;
root = IORegistryGetRootEntry(kIOMainPortDefault);
if (root == MACH_PORT_NULL) {
raw ? exit(1) : puts("--");
return 0;
}

/* Recursive search over the IOService plane; matching=NULL → any entry.
* Returns a retained CFNumber when found. */
CFTypeRef stats = IORegistryEntrySearchCFProperty(
root, kIOServicePlane, STATS_KEY, NULL, kIORegistryIterateRecursively);
CFTypeRef value = NULL;
if (stats != NULL) {
if (CFGetTypeID(stats) == CFDictionaryGetTypeID()) {
value = CFDictionaryGetValue(stats, UTIL_KEY);
if (value != NULL) CFRetain(value);
}
CFRelease(stats);
}

if (value == NULL) {
/* No discrete/integrated GPU stats exposed (e.g. headless VM) */
raw ? exit(1) : puts("--");
return 0;
}

if (CFGetTypeID(value) == CFNumberGetTypeID()) {
int pct = -1;
if (CFNumberGetValue(value, kCFNumberIntType, &pct) && pct >= 0) {
printf("%d\n", pct);
CFRelease(value);
return 0;
}
}
CFRelease(value);
raw ? exit(1) : puts("--");
return 0;
}
5 changes: 3 additions & 2 deletions config/sidebar.toml.snippet
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# $sys_path — focused pane cwd (basename, 32-char truncated)
# $sys_zoom — " zoom " when the focused pane is zoomed, else empty
# $sys_prefix — prefix-active indicator (v0.1: always empty; v0.2)
# $sys_gpu — GPU usage 0-100% (rootless, Apple Silicon; "--" without GPU)
# $sys_cpu — CPU usage 0-100%
# $sys_mem — active+wired memory in GB
# $sys_batt — battery glyph + percentage
Expand All @@ -37,7 +38,7 @@ rows = [
["state_icon", "workspace"],
["branch", "git_status"],
["$sys_path", "$sys_zoom", "$sys_prefix"],
["$sys_cpu", "$sys_mem", "$sys_batt"],
["$sys_net", "$sys_disk"],
["$sys_gpu", "$sys_cpu", "$sys_mem"],
["$sys_batt", "$sys_net", "$sys_disk"],
["$sys_date", "$sys_time"],
]
17 changes: 17 additions & 0 deletions scripts/metrics.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ sys_date() {
date "+%a %b %d"
}

# GPU utilization as 0-100% — rootless via the IORegistry.
# Uses the bundled gpu-util helper (pure IOKit user-space API, no sudo,
# no powermetrics). Searches for the accelerator's PerformanceStatistics
# property instead of matching a chip-specific class name, so it works
# across GPU generations. Falls back to "--" like every other collector.
sys_gpu() {
local pct
pct=$("$SYSMON_ROOT/bin/gpu-util" 2>/dev/null)
if [[ "$pct" =~ ^[0-9]+$ ]]; then
printf "%s%%" "$pct"
else
echo "--"
fi
}

# Focused pane cwd, basename only

# Focused pane cwd, basename only, truncated to 32 chars with leading ellipsis.
# Mirrors the tmux #{=/-32/...:#{b:pane_current_path}} status-left widget.
# Best-effort: the `pane current` JSON shape is not fully documented; we try
Expand Down
12 changes: 9 additions & 3 deletions scripts/poll.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SYSMON_ROOT="$PLUGIN_ROOT"

STATE_DIR="${HERDR_PLUGIN_STATE_DIR:-$PLUGIN_ROOT/.state}"
mkdir -p "$STATE_DIR"
Expand All @@ -41,16 +42,19 @@ log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >>"$LOGFILE"; }
# NOTE: herdr CLI emits JSON by default; there is no --json flag.
list_workspace_ids() {
"$HERDR" workspace list 2>/dev/null | jq -r '
(.result.workspaces // .workspaces // []) | .[].workspace_id // empty
# herdr >= 0.8: workspaces carry no workspace_id; the id is the workspace
# half of active_tab_id ("w3:t1" -> "w3").
(.result.workspaces // .workspaces // [])
| .[] | (.active_tab_id // "" | split(":")[0]) // .label // empty
' 2>/dev/null
}

# Collect every metric once, then push the full token set to every workspace.
push_all() {
local cpu mem batt net disk time date path zoom prefix
local cpu mem batt net disk time date path zoom prefix gpu
cpu=$(sys_cpu); mem=$(sys_mem); batt=$(sys_batt); net=$(sys_net)
disk=$(sys_disk); time=$(sys_time); date=$(sys_date); path=$(sys_path)
zoom=$(sys_zoom); prefix=$(sys_prefix)
zoom=$(sys_zoom); prefix=$(sys_prefix); gpu=$(sys_gpu)

# Never-empty guards. The collectors already fall back, but a total command
# failure (e.g. disk full, binary missing) can still yield an empty string;
Expand All @@ -63,6 +67,7 @@ push_all() {
[[ -z "$time" ]] && time="--"
[[ -z "$date" ]] && date="--"
[[ -z "$path" ]] && path="--"
[[ -z "$gpu" ]] && gpu="--"

local wids wid
wids=$(list_workspace_ids)
Expand All @@ -73,6 +78,7 @@ push_all() {

for wid in $wids; do
"$HERDR" workspace report-metadata "$wid" --source "$SOURCE_ID" \
--token "sys_gpu=$gpu" \
--token "sys_cpu=$cpu" \
--token "sys_mem=$mem" \
--token "sys_batt=$batt" \
Expand Down