About this project
vitals is a system monitor for Apple Silicon Macs running macOS 14 or later. A single Rust binary samples CPU, GPU, memory, power and thermal state in-process: it does not shell out to powermetrics, does not require sudo, and starts no subprocess of any kind. Output is structured JSON that a script, an agent or a person can read directly. The same binary, run with no arguments, is also a menu bar tray item with an optional local dashboard.
Command-line verbs
snapshot prints one full SoC sample: host chip and core counts, system-wide CPU utilization, per-core entries with core kind (efficiency or performance), frequency and utilization, GPU utilization and frequency, memory and swap totals and usage, kernel memory pressure, power figures for CPU, GPU, ANE, RAM and system, CPU and GPU temperatures, fan speeds with maximums, load averages, uptime and thermal state. An interval option changes the sampling window over which CPU, GPU and power residency are integrated.
top ranks processes by CPU and by memory, independently and with a configurable row count, so the same process may appear in both lists. CPU percentage is per process rather than system-wide, and can exceed 100 on multi-core work, like ps.
pressure returns a verdict instead of raw numbers: a state, a flag for whether usable history exists, a list of reasons with codes and severities, a list of suspect processes with the reason each was flagged, and a one-line summary. By design it exits zero when reporting a bad state, since reporting is not failing. An exit-code flag folds the verdict severity into the status instead (0 nominal, 3 warning, 4 critical), while a runtime failure exits 1 and a malformed invocation exits 2.
watch is the only verb that does not exit on its own. It emits one compact JSON object per line, flushed after every line, and runs until interrupted or until a requested number of samples is emitted. The interval sets both the cadence and the sampling window. Because output is flushed per line, a consumer that reads one line and closes the pipe gets a clean exit. It is always newline-delimited JSON and rejects the JSON flag.
JSON is the default for snapshot, top and pressure; each also accepts a human-readable text form for terminals, and the JSON flag is accepted as a no-op. Every response carries a schema version. Units live in key names (memory in MB, power in watts, temperatures in Celsius) rather than in separate fields, and absent optional fields are omitted rather than emitted as null.
Menu bar app and dashboard
Run without arguments, vitals is a menu bar tray with live text and a custom dropdown covering CPU (a sparkline plus one bar per core, efficiency and performance cores tinted differently), GPU, memory judged by the kernel pressure signal, and power. Headline values change color at thresholds, nothing redraws while the dropdown is closed, and the sampling cadence backs off while the display sleeps.
The app declares itself as an accessory, so it has no Dock icon and no app-switcher entry until a window is requested. A dashboard can be opened from the dropdown or by reopening the running app, and it opens in a native window backed by a web view, as its own process. While it is open it has a Dock tile and app menu; when it closes, that process exits, which keeps a closed dashboard at zero cost. If the tray quits or crashes, the window notices via a file-descriptor watch and exits promptly. The HTTP server runs inside the tray process and starts on demand, so a tray that has never been asked for the dashboard has no listener and no second sampler.
The dashboard page is built to be read top-down: the same verdict that the pressure verb produces, four tiles for CPU, GPU, memory and power with headline figure, status dot, context line and trend, a range control for 2, 5 or 15 minutes that scopes everything below it, CPU and GPU charts over time with a crosshair and tooltip, one bar per core labelled by position with clock speed on hover, memory and swap meters whose fill follows the kernel pressure signal, and top processes with inline bars. Every chart has a chart-or-table toggle for the same data, polling stops when the page is hidden, the last good render stays on screen dimmed if the server goes away, and light or dark theme follows the system.
Installation and packaging
Two independent install paths end with vitals on the PATH. A build followed by an install symlinks the release binary into a prefix bin directory (override the prefix to avoid sudo, and an uninstall target removes the symlink). The app install path builds, bundles, copies the app into the Applications folder, installs a per-user launch agent so the tray starts at every login, and repoints the same symlink at the bundle's copy; a matching uninstall target reverses all of it. The bundle is ad-hoc signed rather than notarized, which is sufficient for launch-agent execution; opening the app through Finder may require a one-time right-click Open or approval in System Settings.
Development
Build, test, clippy-with-warnings-as-errors and formatting have make targets. The dashboard is a Vite and React app whose production build output is embedded into the binary at compile time, so the binary must be rebuilt after the page. A dev server with hot reload proxies API requests to the local port, so it works against any running instance, including the one inside the tray.
Contributing is through bug reports, measurements and pull requests, with a separate channel for vulnerabilities. The project is MIT licensed.
Comments
0 Rating appears after 10 ratings
Sign in to join the discussion.