π»π Real-time power tree TUI for Apple Silicon
Rust
987
115 commits
updated May 11, 2026
Real-time power consumption monitor for Apple Silicon Macs (M1βM5+).
macpow reads directly from macOS hardware interfaces β IOReport, SMC, IORegistry, CoreAudio, and Mach/kernel APIs β to show per-component power draw, temperatures, frequencies, CPU utilization, and per-process energy attribution. No sudo required.
| Symbol | Meaning |
|---|---|
0.123 W | Measured power (direct hardware reading) |
β0.123 W | Estimated power (model-based calculation) |
β€0.123 W | Upper-bound power estimate |
βΈ | Pinned resource (sparkline chart visible) |
ββββββββββ | CPU core utilization bar (filled = busy) |
37Β°C | Fresh temperature reading |
~37Β°C | Stale temperature (sensor read failed, showing last known value) |
pending⦠| Data source still initializing |
[dead] | Process has exited (energy total preserved) |
| Bold white | Section headers and measured values |
| Green | Low power (< 1W) or info-only rows |
| Yellow | Moderate power (1β5W) |
| Orange | High power (5β10W) |
| Red | Very high power (> 10W) |
| Gray | Dimmed/inactive items |
host_processor_info~ indicator when sensors temporarily read invalidhost_statistics64 Mach APIAppleARMBacklight IOReport DPB factor, plus IOReport SoC display controller and external display power via DISPEXTproc_pid_rusage), with per-process disk I/O rates, network traffic (via nettop), RAM footprint, dead process detection+/- for allcargo install macpow
git clone https://github.com/k06a/macpow.git
cd macpow
cargo build --release
./target/release/macpow
brew tap k06a/tap
brew install macpow
pixi global install macpow
# execute without installing
pixi exec macpow
macpow # TUI mode (default)
macpow --json # JSON output to stdout
macpow --interval 500 # Set sampling interval in ms (default: 250)
macpow --dump # Dump IOReport channel names (diagnostics)
macpow --dump-smc # Dump every SMC key with type and value (diagnostics)
| Key | Action |
|---|---|
q / Esc | Quit |
Up / Down / j / k | Move cursor |
Left / Right / h | Collapse / expand tree node |
+ / = | Expand all nodes |
- | Collapse all nodes |
Space | Pin/unpin resource chart |
a | Cycle SMA window: 0s / 5s / 10s |
l | Cycle refresh interval: 250ms / 500ms / 1s / 2s |
r | Reset all totals and min/max |
PgUp / PgDn | Scroll by 10 rows |
Home | Jump to top |
| Mouse click | Select row |
All letter keys work on any keyboard layout (QWERTY, Russian, Dvorak, etc).
Each data source runs in its own thread, updating shared metrics at its own pace. The TUI renders at the configured interval without blocking on slow sources.
+------------------+---------------------------------------------+
| Data source | What it provides |
+------------------+---------------------------------------------+
| IOReport | SoC power (Energy Model), |
| | CPU/GPU frequencies (DVFS residency) |
| SMC | System power (PSTR), display backlight |
| | (PBwo on M5/Neo, PDBR on M1-M4), |
| | adapter (PDTR), WiFi (wiPm), temps, fans |
| IORegistry | Battery, display brightness, keyboard PWM, |
| | USB devices, SSD model, disk I/O counters |
| CoreAudio | Volume level, mute state |
| Mach API | Per-CPU utilization ticks, memory stats |
| proc_pid_rusage | Per-process billed energy |
| getifaddrs | Network traffic byte counters |
| CoreWLAN/pmset | WiFi info, Bluetooth devices |
| IOPMAssertions | Power assertions, audio playback detection |
+------------------+---------------------------------------------+
| Component | Source | Method |
|---|---|---|
| CPU, GPU, ANE, DRAM | IOReport | Direct energy measurement (mJ/uJ/nJ deltas) |
| Media Engine, Camera (ISP) | IOReport | Direct energy measurement (AVE + MSR, ISP) |
| Fabric (AMCC, DCS, FAB, AFR) | IOReport | Direct energy measurement |
| Thunderbolt/PCIe | IOReport | Direct energy measurement (PCIe ports + controllers) |
| Display backlight | SMC PBwo (M5 Pro/Max/Neo, A18) / PDBR (M1-M4 XDR) | Direct power rail measurement |
| Display controller | IOReport DISP/DISPEXT | Direct energy measurement (SoC + external) |
| Power adapter | SMC PDTR | Direct power delivery measurement |
| System total | SMC PSTR | Direct power rail measurement |
| Battery | IORegistry | V * I calculation |
| Per-process | Kernel | ri_billed_energy from rusage_info_v4 |
| Per-process disk I/O | Kernel | ri_diskio_bytesread/written from rusage_info_v4 |
| Per-process memory | Kernel | ri_phys_footprint from rusage_info_v4 |
| Per-process network | nettop | Cumulative bytes per process (~18ms subprocess) |
| CPU utilization | Mach API | host_processor_info tick deltas |
| Memory | Mach API | host_statistics64 (active + inactive + wired + compressor pages) |
| Keyboard | IORegistry PWM | Duty cycle * 0.5W max |
| Fans | SMC RPM | Cubic model: (RPM/RPM_max)^3 * 1W |
| Audio | CoreAudio + IOPMAssertions | Idle 0.05W + volume^2 * 1W |
| WiFi | SMC wiPm | Direct power measurement |
| Bluetooth | pmset | Fixed per device type (0.01-0.05W) |
| SSD | IORegistry counters | I/O utilization: 0.03-2.5W |
| Ethernet | getifaddrs | Link detection, speed, per-interface traffic (data only) |
| Network | getifaddrs | Per-interface byte counters for Ethernet and WiFi |
| USB | IORegistry PowerOutDetails | Per-port power measurement (Watts/PDPowermW) |
IOReport channel names vary between single-die and multi-die (Ultra) chips. The parser handles both generically:
Single-die (M1/M2/M3/M4 base/Pro/Max):
CPU Stats: ECPU0, PCPU10 β digit suffix
Energy Model: EACC_CPU0, PACC0_CPU5 β _CPU suffix
Blocks: ISP, DRAM, ANE, DISP β bare names
Multi-die (M1/M2/M3 Ultra):
CPU Stats: DIE_0_ECPU_CPU0, DIE_1_PCPU1_CPU3 β DIE_N_ prefix + _CPU suffix
Energy Model: DIE_0_EACC_CPU0, DIE_1_PACC1_CPU3 β DIE_N_ prefix + _CPU suffix
Blocks: ISP0_0, DRAM0_1, ANE0_0 β per-die suffix
Two design rules keep this forward-compatible with future chips:
strip_die_prefix generically removes DIE_{N}_ so downstream parsers always see the same base formatstarts_with matching for block power handles any suffix Apple may add (e.g. ISP matches ISP, ISP0_0, ISP0_1, etc.)If a new chip isn't detected correctly, run macpow --dump to see the raw IOReport channel names.
Toolchain pinning. The repo ships a
rust-toolchain.tomlthat pinsstable, matchingdtolnay/rust-toolchain@stablein CI. Runrustup update stablebefore each release so local clippy/fmt see the same lints CI does β newly-promoted clippy lints have caused CI failures aftercargo publishalready uploaded the broken version (immutable), so this step is non-optional.
# 0. Make sure the toolchain matches CI exactly
rustup update stable
rustc --version # note the version; CI is on the same `stable`
# 1. Bump version
vim Cargo.toml # update version = "X.Y.Z"
# 2. Build, lint, test β must all pass before the version bump commit
cargo fmt --check # formatting clean
# Clippy: copy this command verbatim from .github/workflows/ci.yml.
# CI does NOT pass --all-targets, so neither do we β adding it pulls in
# benches/examples that CI ignores and would block the release.
cargo clippy -- -D warnings -A clippy::field_reassign_with_default -A clippy::manual_c_str_literals -A clippy::manual_clamp -A clippy::manual_range_contains -A clippy::missing_safety_doc -A clippy::needless_range_loop
cargo test # all tests pass
cargo build --release # final binary build
# 3. Dry-run the publish so any cargo-side issues show up before tagging
cargo publish --dry-run
# 4. Update Homebrew badge in README.md
# Change: homebrew-vX.Y.Z in the badge URL
# 5. Commit, tag, push
git add -A
git commit -m "Bump version to X.Y.Z"
git tag vX.Y.Z
git push origin main --tags
# CI will auto-create GitHub Release with binary
# 6. Publish to crates.io (immutable β only after CI is green!)
cargo publish
# 7. Update Homebrew tap (via PR to trigger bottle building)
curl -sL https://github.com/k06a/macpow/archive/refs/tags/vX.Y.Z.tar.gz | shasum -a 256
# Update url + sha256 in homebrew-tap/Formula/macpow.rb
cd ../homebrew-tap
git checkout main && git pull
git checkout -b update-macpow-X.Y.Z
# edit Formula/macpow.rb (url + sha256 only β bottle block is auto-rewritten)
git commit -am "Update macpow to X.Y.Z"
git push origin update-macpow-X.Y.Z
# Create PR, wait for CI to build bottles, then add label "pr-pull"
# publish.yml will upload bottles and merge the PR
# 8. Update conda-forge feedstock
cd ../macpow-feedstock
git checkout main && git pull origin main
git checkout -b bump-X.Y.Z
# edit recipe/recipe.yaml: bump `version:` and update `sha256:` (same value as step 7)
git commit -am "macpow vX.Y.Z"
git push fork bump-X.Y.Z # fork remote, since origin is conda-forge upstream
# Open PR against conda-forge/macpow-feedstock from your fork branch
If CI fails AFTER you ran cargo publish (step 6), that crates.io version
is immutable. Yank it (cargo yank --version X.Y.Z) and bump the patch
number β don't try to force-push the tag.
Before submitting a PR, please run:
cargo fmt --check # code formatting
# Clippy: must match CI (see .github/workflows/ci.yml)
cargo clippy -- -D warnings -A clippy::field_reassign_with_default -A clippy::manual_c_str_literals -A clippy::manual_clamp -A clippy::manual_range_contains -A clippy::missing_safety_doc -A clippy::needless_range_loop
cargo test # unit + integration tests
cargo build --release # final build check
All four must pass with zero errors. Clippy needs the flags above so local runs match CI; without them, stable Clippy may report project-wide warnings that CI intentionally allows (FFI / Objective-C style).
If per-core temperatures are missing or incorrect on your Mac, please open an issue with:
macpow --dump > dump.txt # IOReport channel names
macpow --dump-smc > smc.txt # all SMC keys (incl. power rails)
macpow --json > metrics.json # full metrics (Ctrl+C after ~15s)
system_profiler SPHardwareDataType | head -10 # chip model
This helps add support for new Apple Silicon variants.
3,979 followers Β· starred Mar 2026
409 followers Β· starred Apr 2026
1,503 followers Β· starred Mar 2026
494 followers Β· starred May 2026
π»π Real-time power tree TUI for Apple Silicon
Rust
987
115 commits
updated May 11, 2026
Real-time power consumption monitor for Apple Silicon Macs (M1βM5+).
macpow reads directly from macOS hardware interfaces β IOReport, SMC, IORegistry, CoreAudio, and Mach/kernel APIs β to show per-component power draw, temperatures, frequencies, CPU utilization, and per-process energy attribution. No sudo required.
| Symbol | Meaning |
|---|---|
0.123 W | Measured power (direct hardware reading) |
β0.123 W | Estimated power (model-based calculation) |
β€0.123 W | Upper-bound power estimate |
βΈ | Pinned resource (sparkline chart visible) |
ββββββββββ | CPU core utilization bar (filled = busy) |
37Β°C | Fresh temperature reading |
~37Β°C | Stale temperature (sensor read failed, showing last known value) |
pending⦠| Data source still initializing |
[dead] | Process has exited (energy total preserved) |
| Bold white | Section headers and measured values |
| Green | Low power (< 1W) or info-only rows |
| Yellow | Moderate power (1β5W) |
| Orange | High power (5β10W) |
| Red | Very high power (> 10W) |
| Gray | Dimmed/inactive items |
host_processor_info~ indicator when sensors temporarily read invalidhost_statistics64 Mach APIAppleARMBacklight IOReport DPB factor, plus IOReport SoC display controller and external display power via DISPEXTproc_pid_rusage), with per-process disk I/O rates, network traffic (via nettop), RAM footprint, dead process detection+/- for allcargo install macpow
git clone https://github.com/k06a/macpow.git
cd macpow
cargo build --release
./target/release/macpow
brew tap k06a/tap
brew install macpow
pixi global install macpow
# execute without installing
pixi exec macpow
macpow # TUI mode (default)
macpow --json # JSON output to stdout
macpow --interval 500 # Set sampling interval in ms (default: 250)
macpow --dump # Dump IOReport channel names (diagnostics)
macpow --dump-smc # Dump every SMC key with type and value (diagnostics)
| Key | Action |
|---|---|
q / Esc | Quit |
Up / Down / j / k | Move cursor |
Left / Right / h | Collapse / expand tree node |
+ / = | Expand all nodes |
- | Collapse all nodes |
Space | Pin/unpin resource chart |
a | Cycle SMA window: 0s / 5s / 10s |
l | Cycle refresh interval: 250ms / 500ms / 1s / 2s |
r | Reset all totals and min/max |
PgUp / PgDn | Scroll by 10 rows |
Home | Jump to top |
| Mouse click | Select row |
All letter keys work on any keyboard layout (QWERTY, Russian, Dvorak, etc).
Each data source runs in its own thread, updating shared metrics at its own pace. The TUI renders at the configured interval without blocking on slow sources.
+------------------+---------------------------------------------+
| Data source | What it provides |
+------------------+---------------------------------------------+
| IOReport | SoC power (Energy Model), |
| | CPU/GPU frequencies (DVFS residency) |
| SMC | System power (PSTR), display backlight |
| | (PBwo on M5/Neo, PDBR on M1-M4), |
| | adapter (PDTR), WiFi (wiPm), temps, fans |
| IORegistry | Battery, display brightness, keyboard PWM, |
| | USB devices, SSD model, disk I/O counters |
| CoreAudio | Volume level, mute state |
| Mach API | Per-CPU utilization ticks, memory stats |
| proc_pid_rusage | Per-process billed energy |
| getifaddrs | Network traffic byte counters |
| CoreWLAN/pmset | WiFi info, Bluetooth devices |
| IOPMAssertions | Power assertions, audio playback detection |
+------------------+---------------------------------------------+
| Component | Source | Method |
|---|---|---|
| CPU, GPU, ANE, DRAM | IOReport | Direct energy measurement (mJ/uJ/nJ deltas) |
| Media Engine, Camera (ISP) | IOReport | Direct energy measurement (AVE + MSR, ISP) |
| Fabric (AMCC, DCS, FAB, AFR) | IOReport | Direct energy measurement |
| Thunderbolt/PCIe | IOReport | Direct energy measurement (PCIe ports + controllers) |
| Display backlight | SMC PBwo (M5 Pro/Max/Neo, A18) / PDBR (M1-M4 XDR) | Direct power rail measurement |
| Display controller | IOReport DISP/DISPEXT | Direct energy measurement (SoC + external) |
| Power adapter | SMC PDTR | Direct power delivery measurement |
| System total | SMC PSTR | Direct power rail measurement |
| Battery | IORegistry | V * I calculation |
| Per-process | Kernel | ri_billed_energy from rusage_info_v4 |
| Per-process disk I/O | Kernel | ri_diskio_bytesread/written from rusage_info_v4 |
| Per-process memory | Kernel | ri_phys_footprint from rusage_info_v4 |
| Per-process network | nettop | Cumulative bytes per process (~18ms subprocess) |
| CPU utilization | Mach API | host_processor_info tick deltas |
| Memory | Mach API | host_statistics64 (active + inactive + wired + compressor pages) |
| Keyboard | IORegistry PWM | Duty cycle * 0.5W max |
| Fans | SMC RPM | Cubic model: (RPM/RPM_max)^3 * 1W |
| Audio | CoreAudio + IOPMAssertions | Idle 0.05W + volume^2 * 1W |
| WiFi | SMC wiPm | Direct power measurement |
| Bluetooth | pmset | Fixed per device type (0.01-0.05W) |
| SSD | IORegistry counters | I/O utilization: 0.03-2.5W |
| Ethernet | getifaddrs | Link detection, speed, per-interface traffic (data only) |
| Network | getifaddrs | Per-interface byte counters for Ethernet and WiFi |
| USB | IORegistry PowerOutDetails | Per-port power measurement (Watts/PDPowermW) |
IOReport channel names vary between single-die and multi-die (Ultra) chips. The parser handles both generically:
Single-die (M1/M2/M3/M4 base/Pro/Max):
CPU Stats: ECPU0, PCPU10 β digit suffix
Energy Model: EACC_CPU0, PACC0_CPU5 β _CPU suffix
Blocks: ISP, DRAM, ANE, DISP β bare names
Multi-die (M1/M2/M3 Ultra):
CPU Stats: DIE_0_ECPU_CPU0, DIE_1_PCPU1_CPU3 β DIE_N_ prefix + _CPU suffix
Energy Model: DIE_0_EACC_CPU0, DIE_1_PACC1_CPU3 β DIE_N_ prefix + _CPU suffix
Blocks: ISP0_0, DRAM0_1, ANE0_0 β per-die suffix
Two design rules keep this forward-compatible with future chips:
strip_die_prefix generically removes DIE_{N}_ so downstream parsers always see the same base formatstarts_with matching for block power handles any suffix Apple may add (e.g. ISP matches ISP, ISP0_0, ISP0_1, etc.)If a new chip isn't detected correctly, run macpow --dump to see the raw IOReport channel names.
Toolchain pinning. The repo ships a
rust-toolchain.tomlthat pinsstable, matchingdtolnay/rust-toolchain@stablein CI. Runrustup update stablebefore each release so local clippy/fmt see the same lints CI does β newly-promoted clippy lints have caused CI failures aftercargo publishalready uploaded the broken version (immutable), so this step is non-optional.
# 0. Make sure the toolchain matches CI exactly
rustup update stable
rustc --version # note the version; CI is on the same `stable`
# 1. Bump version
vim Cargo.toml # update version = "X.Y.Z"
# 2. Build, lint, test β must all pass before the version bump commit
cargo fmt --check # formatting clean
# Clippy: copy this command verbatim from .github/workflows/ci.yml.
# CI does NOT pass --all-targets, so neither do we β adding it pulls in
# benches/examples that CI ignores and would block the release.
cargo clippy -- -D warnings -A clippy::field_reassign_with_default -A clippy::manual_c_str_literals -A clippy::manual_clamp -A clippy::manual_range_contains -A clippy::missing_safety_doc -A clippy::needless_range_loop
cargo test # all tests pass
cargo build --release # final binary build
# 3. Dry-run the publish so any cargo-side issues show up before tagging
cargo publish --dry-run
# 4. Update Homebrew badge in README.md
# Change: homebrew-vX.Y.Z in the badge URL
# 5. Commit, tag, push
git add -A
git commit -m "Bump version to X.Y.Z"
git tag vX.Y.Z
git push origin main --tags
# CI will auto-create GitHub Release with binary
# 6. Publish to crates.io (immutable β only after CI is green!)
cargo publish
# 7. Update Homebrew tap (via PR to trigger bottle building)
curl -sL https://github.com/k06a/macpow/archive/refs/tags/vX.Y.Z.tar.gz | shasum -a 256
# Update url + sha256 in homebrew-tap/Formula/macpow.rb
cd ../homebrew-tap
git checkout main && git pull
git checkout -b update-macpow-X.Y.Z
# edit Formula/macpow.rb (url + sha256 only β bottle block is auto-rewritten)
git commit -am "Update macpow to X.Y.Z"
git push origin update-macpow-X.Y.Z
# Create PR, wait for CI to build bottles, then add label "pr-pull"
# publish.yml will upload bottles and merge the PR
# 8. Update conda-forge feedstock
cd ../macpow-feedstock
git checkout main && git pull origin main
git checkout -b bump-X.Y.Z
# edit recipe/recipe.yaml: bump `version:` and update `sha256:` (same value as step 7)
git commit -am "macpow vX.Y.Z"
git push fork bump-X.Y.Z # fork remote, since origin is conda-forge upstream
# Open PR against conda-forge/macpow-feedstock from your fork branch
If CI fails AFTER you ran cargo publish (step 6), that crates.io version
is immutable. Yank it (cargo yank --version X.Y.Z) and bump the patch
number β don't try to force-push the tag.
Before submitting a PR, please run:
cargo fmt --check # code formatting
# Clippy: must match CI (see .github/workflows/ci.yml)
cargo clippy -- -D warnings -A clippy::field_reassign_with_default -A clippy::manual_c_str_literals -A clippy::manual_clamp -A clippy::manual_range_contains -A clippy::missing_safety_doc -A clippy::needless_range_loop
cargo test # unit + integration tests
cargo build --release # final build check
All four must pass with zero errors. Clippy needs the flags above so local runs match CI; without them, stable Clippy may report project-wide warnings that CI intentionally allows (FFI / Objective-C style).
If per-core temperatures are missing or incorrect on your Mac, please open an issue with:
macpow --dump > dump.txt # IOReport channel names
macpow --dump-smc > smc.txt # all SMC keys (incl. power rails)
macpow --json > metrics.json # full metrics (Ctrl+C after ~15s)
system_profiler SPHardwareDataType | head -10 # chip model
This helps add support for new Apple Silicon variants.
3,979 followers Β· starred Mar 2026
409 followers Β· starred Apr 2026
1,503 followers Β· starred Mar 2026
494 followers Β· starred May 2026