Tags: wdes/wattaouille
Tags
fix: detect display off via DRM connector DPMS too (v1.3.5) `bl_power` (the legacy framebuffer interface) doesn't always flip when the modern desktop blanks the screen. XFCE Power Manager's "blank screen", `xset dpms force off`, and GNOME's idle-blank all toggle the DRM connector's DPMS state at /sys/class/drm/card<N>-eDP-<M>/dpms but leave bl_power at 0. v1.3.1 only checked bl_power, so the 🖥 line kept reporting `~1.4 W (30% bl)` even though the panel was actually dark. BacklightSensor::detect now also enumerates /sys/class/drm/*-eDP-*/ and /sys/class/drm/*-LVDS-*/ connectors that are `enabled` + `connected`, captures their `dpms` files, and is_powered_off() returns true if EITHER bl_power says non-zero OR any captured dpms file reports anything other than "On". The detection scan only happens once at startup (the connector list is stable for the lifetime of a session). Per-frame cost is one extra read syscall per active panel. Tests: +2 (DRM-DPMS-off path with bl_power still On; both-on baseline to ensure the readout works through the new code path). 51 total. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
feat: show version in header + --version flag (v1.3.4) The first line of the live display now starts with `wattaouille vX.Y.Z` so you can tell at a glance which build you're looking at — handy when multiple binaries are floating around or when reproducing an issue from a screenshot. Also added a `-V` / `--version` flag that prints the version and exits, matching the convention of every other CLI. Version is pulled from CARGO_PKG_VERSION at compile time, so there's nothing to keep in sync — bumping Cargo.toml is the single source. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
fix: stdin raw mode prevents echo'd scroll keys in alt screen (v1.3.3) Scrolling with the mouse wheel (or hitting arrow keys) inside the running wattaouille produced visible garbage on the alt screen. Cause: those events arrive on stdin as `\x1B[A` / `\x1B[B` etc., and with canonical+echo mode (the default for any tty) the kernel tty driver echoed them back to fd 1 — which is the alt screen we're drawing on. Fix: capture the original termios at startup, then flip stdin to non-canonical / non-echo right after entering the alt screen. The ctrlc handler restores it before exiting (via OnceLock<termios> for safe lock-free read from the handler thread). `libc = "0.2"` added as an explicit dep — it was already pulled in by `ctrlc`'s nix dependency, so this is a zero-cost-on-disk change. CHANGELOG updated. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
feat: Element + Claude shell labels, CHANGELOG.md (v1.3.2)
Element (Matrix) desktop
comm "element-desktop" (and the un-truncated "Element") now render as
"Element (Matrix)".
Claude shell
Every Claude Code tool call spawns a fresh
bash -c source /home/.../.claude/shell-snapshots/snapshot-bash-X.sh \
2>/dev/null || true && shopt -u extglob && eval '<cmd>'
Those used to surface as identical "bash -c source …" lines (one per
call, indistinguishable). Now collapsed to "Claude shell (<cwd>)" so
parallel projects are obvious.
CHANGELOG.md
Initial changelog backfilling every release from v1.0.0 to v1.3.2.
Format follows Keep a Changelog. README links to it.
Tests
+3 (Element label, Claude shell with cwd, Claude shell without cwd).
49 total.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
fix: backlight + Wi-Fi power-off detection, width-stable wattage (v1.…
…3.1)
Backlight off
v1.3.0 always reported a display watts estimate as long as a
/sys/class/backlight/* device existed. Turning the panel off (Fn-Fx
or DPMS blank) sets that device's bl_power to a non-zero state
(1=NORMAL/2=VSYNC/3=HSYNC/4=POWERDOWN), but brightness can stay at
its last value, so we kept estimating ~1.4 W of phantom panel draw.
BacklightSensor now also captures the bl_power path. is_powered_off()
returns true on any non-zero value, in which case estimated_watts()
returns None and the 🖥 line disappears entirely.
Wi-Fi off (rfkill)
v1.3.0 used `operstate` to decide whether the Wi-Fi was associated.
rfkill keeps operstate at "down" but the sysfs `carrier` file is the
authoritative signal: 1 when there's an active link, 0 otherwise.
Switched NetIface to read `carrier`, and the 📡 Wi-Fi line is now
hidden (returns None instead of Some(0.0)) when nothing's linked.
Width-stable wattage
Per "ensure wattage has always space for 3 numbers": all wattage
values now format as {:>5.1} (5 chars wide → " 0.5", " 12.5",
"123.5") so the bits to the right don't shift between frames as
values cross 10 W or 100 W. Drift uses {:>+6.1} for the sign.
Affects:
⚡ RAPL X.X W avg (core X.X W · uncore X.X W · dram X.X W)
🔋 BAT X.X W avg
🖥 ~X.X W
📡 Wi-Fi ~X.X W
Δ non-CPU ±X.X W avg
Tests
+1: backlight_powered_off_returns_none uses a tmp file with
bl_power=4 to verify the off-state path. Renamed
backlight_watts_at_zero_is_baseline → ..._no_device_returns_none for
clarity (the original name was misleading; it actually tests the
no-path branch). 46 total, all passing.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
feat: non-CPU energy attribution — RAPL subdomains, I/O, display, Wi-…
…Fi (v1.3.0)
The big v1.2 → v1.3 bump: wattaouille now reports the things that
weren't visible before, on top of the CPU package draw.
RAPL subdomain breakout
PowerSensor::detect now also enumerates intel-rapl:0:* (core, uncore,
dram). Each gets its own RaplDomain with independent wraparound. The
header shows a per-frame "now" breakdown:
⚡ RAPL 12.5 W avg (core 8.1W · uncore 1.4W · dram 0.6W) · 0.146 Wh (525 J)
Same chmod gates the subdomains as the package counter.
Per-process I/O column
/proc/[pid]/io read_bytes + write_bytes is parsed each frame, deltas
attributed per PID, and the leaderboard gains an "I/O" column with
human-friendly rate (e.g. "74.7 KB/s"). Browser collapse roots sum
their subtree's I/O. /proc/[pid]/io is mode 0400 + ptrace-restricted,
so other users' processes show "—" — degrades silently.
Display panel watt estimate
BacklightSensor reads /sys/class/backlight/*/brightness and models
panel draw with a linear envelope: 0.5 W floor (panel electronics) to
3.5 W full (LED backlight). Shown with `~` prefix to flag the
estimate, plus session Wh:
🖥 ~1.4 W (30% bl · ~0.002 Wh)
Wi-Fi radio watt estimate
NetSensor classifies each interface as wireless (presence of
/sys/class/net/<iface>/wireless or /phy80211 symlink) and tracks
rx+tx bytes separately. estimate_wifi_radio_watts() models radio
draw: 0 W when not associated, 0.7 W idle, ramping linearly to
2.5 W at 5 MB/s aggregated rx+tx. `~` prefix.
📡 Wi-Fi ~0.7 W (350 KB/s · ~0.001 Wh)
Total network throughput
Sum of rx+tx across all non-loopback interfaces, formatted with
fmt_byte_rate (B/s → KB/s → MB/s → GB/s):
📶 net 130.1 KB/s
Estimate-prefix convention
Per user request: any value that's modeled rather than read directly
from a sensor gets a `~` prefix. Currently applies to display watts
and Wi-Fi radio watts.
More pretty labels
Slack desktop, Discord, VS Code, Spotify, Thunderbird.
Tests
9 new tests: parse_proc_io for sums and missing fields, BacklightSensor
with no backlight path, estimate_wifi_radio_watts at three points,
fmt_byte_rate units, RaplDomain wraparound, slack label. 45 total.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
fix: Ctrl+C handler bypassed stdout lock; add MPL-2.0 license (v1.2.2) Ctrl+C deadlock v1.2.1's ctrlc handler called io::stdout().lock().write_all(...) to emit the terminal-restore sequence. The render loop in main holds a StdoutLock for its entire run, so the handler thread blocked forever acquiring the same lock and the program never exited on Ctrl+C. Fix: write the restore sequence directly via libc::write to fd 1. That's one syscall, bypasses the Rust mutex, and lets std::process:: exit(130) actually run. License: MPL-2.0 No license file existed. Added Mozilla Public License v2.0 (file-level copyleft, friendly to embedding inside proprietary tools as long as modifications to wattaouille files themselves are shared back). Cargo.toml `license = "MIT"` placeholder corrected to `"MPL-2.0"`. README license section updated. History was kept linear — added as a new commit rather than rewriting v1.0.0..v1.2.1 because main is already pushed to origin and the rewrite benefit (LICENSE present from the start) doesn't justify a force-push. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
feat: terminal restore on Ctrl+C, two-line header, ~30 more labels (v…
…1.2.1)
Terminal cleanup
Ctrl+C used to kill the program before it could send the alt-screen
exit / cursor-show sequences, leaving the terminal in a state where
the cursor was hidden and (depending on the emulator) typed text was
invisible. Install a SIGINT/SIGTERM handler via the `ctrlc` crate
(added as the first dep) that writes the restore sequence and exits
cleanly with code 130.
Two-line header
The summary line was getting too long once RAPL + BAT + drift were
all present. Split into:
line 1 — runtime stats (interval, CPU count, proc count, elapsed,
Ctrl+C hint)
line 2 — energy bits (RAPL avg/Wh/J, BAT avg/Wh/J/SoC/time-to-empty,
drift)
When power is disabled line 2 just doesn't render.
`pretty_known()` — friendly labels for ~30 common system processes
XFCE: xfdesktop, xfce4-panel, xfce4-session, xfwm4, xfsettingsd,
xfconfd, xfce4-power-mana, xfce4-clipman, xfce4-screensav,
Thunar, lightdm, Xorg
Input methods: ibus-daemon, ibus-ui-gtk3, ibus-engine-sim,
ibus-extension-, ibus-x11
Audio: pipewire, pipewire-pulse, wireplumber, pulseaudio
System bus / journal: dbus-daemon, systemd-journal, systemd-logind,
systemd-udevd
Daemons: dockerd, containerd, redis-server, teamviewerd, scdaemon,
ntp-daemon, warp-taskbar, NetworkManager, ModemManager,
bluetoothd, tailscaled, wpa_supplicant, avahi-daemon,
accounts-daemon, polkitd, udisksd, upowerd, colord,
rtkit-daemon, snapd, cupsd, cups-browsed, smartd, boltd,
xiccd, yubikey-touch-d
Truncated comm names (kernel limits to 15 chars): power-profiles-,
xdg-desktop-por, xdg-document-po, xdg-permission-,
switcheroo-cont, at-spi-bus-laun, at-spi2-registr
Containers/sandboxing: rootlesskit, slirp4netns
Servers: caddy, apache2, crowdsec, anydesk
Misc: agetty, solaar, blueman-tray/applet, smartgit.sh
Argv-aware shortcuts:
containerd-shim-runc-v2 → "containerd-shim (sha[:8])" using -id
wrapper-2.0 …/lib<plugin>.so → "Xfce panel plugin (<plugin>)"
python3 …/blueman-X → "Blueman (X)"
node …/ng serve --port=N → "ng serve (:N)"
Tests
9 new tests covering the new labels (xfce, ibus, pipewire, dockerd,
containerd-shim with/without id, xfce panel plugin, blueman, ng
serve). 36 total, all passing.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
release: rename to wattaouille, energy/UX upgrades, unit tests (v1.2.0)
Project rename
monitor → wattaouille (watt + ratatouille + ouille!).
Cargo crate, binary name, header banner, help text, README and the
`Or just run … with sudo` instruction line all updated. Project
directory moved from @williamdes/monitor → @wdes/wattaouille.
New labels
python3 /usr/bin/guake → "Guake terminal"
mysqld / mariadbd → "mysqld (cwd-basename)" so multiple
instances are easy to tell apart
(cwd usually points at the data dir)
Battery: state of charge + time-to-empty
BatterySensor now reads energy_now and energy_full alongside
power_now. The header gains, while discharging:
🔋 BAT 18.0 W avg · 0.210 Wh (756 J) · 87% · 4h 23m left
And while plugged in:
🔌 on AC · 87%
fmt_hours() prints "Xh YYm".
Drift display
Δ now shown as a rate AND totals, leading with W avg so you can read
it as "the rest of the system is currently pulling X watts":
Δ non-CPU +5.5 W avg · 0.064 Wh (231 J · +31%)
Unit tests
27 tests in `mod tests`, no /proc dependence:
• cwd_basename edge cases (trailing slash, root, missing)
• is_claude_code (claude.exe path AND @anthropic-ai/claude-code in
any argv slot — broadened so `node …/cli.js` is recognised)
• happy_subcommand for claude / daemon / unrelated
• pretty_cmdline for Claude+Happy+cwd, Claude bare, Happy daemon vs
session, SmartGit, Guake, mysqld with/without cwd
• is_collapse_root for librewolf main, opera --type=, librewolf
-contentproc, plain rustc
• PowerSensor::joules_between simple diff and counter wraparound
• fmt_hours basic + invalid (NaN, negative)
• flatten_visible: promotes busy grandchildren past idle wrappers,
keeps collapse roots at zero own CPU, drops fully-dead subtrees
`cargo test --release` runs them all and ties them to CI.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
PreviousNext