diskwatch 0.5.8

Single-host, read-only disk diagnostics TUI. Eight tabs across devices, volumes, filesystems, IO, SMART, hot files, and insights. Sibling to netwatch and syswatch.
diskwatch-0.5.8 is not a library.

Dense

diskwatch --dense

Six boxes, one screen, no chrome rows — every keybind, sort state and page counter lives in a box border.

Box Shows
io Mirrored read/write throughput on a shared time axis. Peak, average, iops split by direction, mean request size, utilisation, await, p99.
devices Per-device read/write, utilisation, size, type, 48s sparkline. Stacked devices (md, dm, LVM, LUKS) are listed but excluded from totals.
latency IO completion histogram, seven buckets from <0.1ms to >50ms, with p50/p95/p99 and the share of ops past 10 ms. Bars are coloured by bucket, not by count, so the tail is visible before it fills.
volumes Capacity meters with a days-to-full projection from observed growth. Silent when a volume is flat or shrinking.
smart Health, wear, host writes, temperature, spare.
files Busiest paths by event rate, with the process holding each one. Filter with /, sort with s.

Falls back to a compact screen below 104×32, keeping the mirror and the percentiles.

Tabs

# Tab Shows Replaces
1 Overview KPI tiles, device summary, aggregate IO, capacity bar —
2 Devices model, firmware, serial, used %, SMART, per-device detail lsblk, nvme list, diskutil list, hdparm -I
3 Volumes APFS containers + roles; mdraid members, [UUUU] state, resync progress lvs, vgs, mdadm --detail, diskutil apfs list
4 FS mounts with usage bars, thresholds, system/user/removable df -h, df -i, mount, findmnt
5 IO per-device throughput, 48s sparkline, p50/p99 read and write iostat -x 1
6 SMART full NVMe/ATA attribute tables when smartctl is present smartctl -A, nvme smart-log
7 Hot Files paths by event rate (FSEvents / inotify), with the process holding each one fatrace, fs_usage, lsof
8 Insights capacity, SMART, wear, temperature, latency and hot-file anomalies —

Who is writing that file

Neither inotify nor FSEvents carries a pid, so the Hot Files PROCESS column is a join, not a measurement: diskwatch reads per-process byte rates (/proc/<pid>/io, proc_pidinfo) and open file descriptors (/proc/<pid>/fd, PROC_PIDLISTFDS), then names the busiest process holding each hot path open. Two limits, both stated on the tab rather than buried here:

  • It is sampled every 2s. A writer that opens, writes and closes between two samples is missed. Compilers, git and package managers are the usual casualties.
  • Unprivileged it sees only your uid. The banner counts the processes it could not read, so a mostly-empty column says why instead of looking broken. sudo diskwatch attributes the rest.

Getting an exact pid per event needs fanotify with FAN_REPORT_PID or eBPF, both root. diskwatch runs as you, so it infers instead and shows its working.

Lite

diskwatch --lite

80×24, six keys, no tabs: read and write throughput, a capacity line that answers how long have I got, and the busiest files. ↵ opens a detail block naming the process holding the selected file; above 99 columns that becomes a PROCESS column of its own. Sized for a tmux split or an SSH session to a NAS. Same grid and keys as netwatch --lite.

Install

brew install diskwatch                # macOS / Linux
nix-shell -p diskwatch                # NixOS / Nix
paru -S diskwatch                     # Arch
cargo install diskwatch               # anywhere with Rust
x eget use matthart1983/diskwatch     # prebuilt release binary

Or a pre-built binary from Releases — Linux and macOS, x86_64 and aarch64, plus static musl builds, plus an armv5te build for older Marvell Kirkwood NAS boxes (e.g. Iomega ix2-dl) running Debian. Windows x86_64 binaries are also available; extract the archive and run diskwatch-windows-x86_64.exe.

No system dependencies on Linux. macOS uses the preinstalled ioreg, diskutil and system_profiler. Optional: smartmontools for full SMART attribute tables; without it the SMART tab falls back to the basic verified/failing flag.

Nix and Arch packages are maintained by community packagers — file packaging issues with them, diskwatch bugs here. Repology shows which are current.

git clone https://github.com/matthart1983/diskwatch.git && cd diskwatch
cargo build --release
./target/release/diskwatch

Rust 1.75+.

Keys

Key Action
1–8 Switch tabs
← → h l / Shift+Tab Tab Previous / next tab (Full view)
V Cycle view: full → lite → dense
L Jump straight to Lite
↑ ↓ j k Move selection
/ Filter files (Lite, Dense)
s Cycle file sort (Dense)
p Pause / resume
, Settings — columns, temperature unit, SMART interval, theme, view
r Force a SMART refresh
? Help
q / Esc Quit

Options

Flag
--dense Start in Dense (--v2, --btop still work)
--lite Start in Lite
--view full, lite, dense
--tab Start on a named tab
--theme terminal (default), dark, light, ocean, solarized, dracula, nord
--graph bars (default) or dots for btop-style braille
--graph-fade btop's brightness gradient and dot grid — needs an RGB theme, so pair with --theme dark
--no-graph-fade Turn the gradient off, against a config file that turns it on
--watch PATH Watch this path in Hot Files instead of the defaults. Repeatable
--watch-add PATH Watch this path in addition to the defaults. Repeatable
--config PATH Read this config file instead of the default location
--no-config Ignore any config file and run on built-in defaults
--write-config Write a commented config file listing every setting, then exit
--diag Print collected state and exit, no TUI

diskwatch defers to your terminal's palette by default. It pins no colours of its own: every slot resolves to an ANSI entry and foreground/background use Reset, so a terminal profile, pywal, matugen or a system-wide rice carries straight through and diskwatch sits beside your other tools instead of fighting them.

Pass --theme dark for diskwatch's own designed palette, or any of the other built-ins. Those pin real RGB, which is what --graph-fade needs to fade through — a 16-colour palette has no intermediate shades, so the gradient does nothing under the default.

Theme and graph style are also live in the settings overlay. Changes there last for the session; the config file below is what makes one stick.

Config

diskwatch runs with zero config. If you want one anyway:

diskwatch --write-config

writes a commented file listing every setting at its default, to $XDG_CONFIG_HOME/diskwatch/config.toml (~/.config/diskwatch/config.toml if that's unset — macOS included). $DISKWATCH_CONFIG or --config PATH points elsewhere; --no-config ignores it entirely.

theme = "nord"
view = "dense"
tab = "hot"
smart_interval_secs = 60
temp_unit = "fahrenheit"
columns = ["size", "free", "used_pct", "temp", "smart"]

# Hot Files roots. Replaces the defaults ($HOME plus the OS log and tmp dirs).
watch_paths = ["~/src", "/var/log"]
# ...or keep the defaults and add to them.
extra_watch_paths = ["/srv/data"]

Precedence: CLI flag > environment variable > config file > built-in default. A flag you just typed always beats a file you wrote months ago.

Nothing in the file is fatal. An unknown key, an unparseable value, an unreadable file: each becomes a warning on stderr, the rest of the file still applies, and diskwatch still starts. --diag reprints the warnings, and the settings overlay says how many there were.

The format is a deliberate subset of TOML — flat key = value, # comments, no tables — so diskwatch parses it without taking on a dependency that would move its MSRV. Every file it accepts is also valid TOML.

Watch paths

The Hot Files tab watches $HOME plus the OS log and tmp directories by default. To point it somewhere else, in descending precedence:

diskwatch --watch ~/src --watch /var/log     # replace the defaults
diskwatch --watch-add /srv/data              # keep them, add one
DISKWATCH_WATCH_PATHS=/srv:/var/log diskwatch

Each root is watched recursively, which on Linux costs one inotify watch per directory underneath it. Point it at a large tree and you can exhaust fs.inotify.max_user_watches — diskwatch names the path and says so rather than reporting the kernel's bare "No space left on device", which sends people looking at df. A root that doesn't exist is named too, and doesn't stop the others being watched.

What's real, what's deferred

Metric macOS Linux
Device model / serial / firmware ✅ system_profiler + IOKit ✅ /sys/block/*/device/*
Per-device used bytes ✅ via APFS container map ✅ summed from mounts
Read/write byte rates, split ✅ IOKit Statistics ✅ /proc/diskstats 5/9
Read/write iops, split ✅ IOKit Operations ✅ /proc/diskstats 4/8
Avg per-op latency ✅ Total Time / Operations ✅ /proc/diskstats 6/10
p50 / p99 latency ✅ tick-averaged over 60s ✅ tick-averaged over 60s
Latency histogram, 7 buckets ✅ tick means weighted by ops ✅ tick means weighted by ops
True per-op p99 ❌ needs IOReport entitlement ❌ needs eBPF biolatency
Device utilisation (%util) ❌ IOKit has no busy-time counter ✅ /proc/diskstats 13
Requests in flight ❌ not exposed by IOKit ✅ /proc/diskstats 12
SMART attributes ✅ smartctl if installed ✅ smartctl if installed
Volumes — APFS / mdraid ✅ diskutil apfs list ✅ /proc/mdstat
Volumes — ZFS, LVM ⏳ deferred ⏳ deferred
Hot files — paths ✅ FSEvents ✅ inotify
Hot files — bytes / pid ❌ needs root or entitlement ❌ needs eBPF biosnoop
Capacity growth + time-to-full ✅ 10-min usage window ✅ 10-min usage window

Anything a platform can't measure renders -- and keeps its column, so the layout never shifts between machines. Two consequences worth knowing:

  • macOS reports no utilisation. IOKit counts service time, not time-with-IO-in-flight, which on a deep-queue NVMe exceeds wall clock. Dense sorts devices by throughput there and says so in its own border.
  • The latency histogram samples. Each 200 ms tick contributes its op count to the bucket holding that tick's mean service time. It shows a sustained slow stretch; it smears a lone 50 ms outlier into whatever its tick averaged.

Anti-goals

  • Not multi-host. Use NetWatch Cloud for a fleet view.
  • Not a daemon. No background collector, no persisted DB.
  • Not a cleaner. It surfaces what's eating disk; it deletes nothing.
  • Not a backup product. Snapshots are observed, not authored.
  • Not a benchmark. It measures what's happening, not what's possible.

License

MIT.