Architecture¶
This page describes the internal architecture of pitop for developers who want to understand or contribute to the codebase.
Design principles¶
-
Direct sysfs/procfs parsing only -- no external system-info libraries (e.g.,
sysinfocrate). All data is read from/proc/and/sys/files directly, or viavcgencmdsubprocess. -
Graceful degradation -- every Pi-specific feature handles missing hardware. The app runs on any Linux system with reduced features.
-
No panics in production -- no
unwrap()orexpect()in any code path that can reach production. Useanyhow::Result,Option, or.unwrap_or_default(). -
Lazy tab refresh -- only the active tab's expensive collectors run each tick. Overview metrics always run.
-
Dynamic hardware discovery -- never hardcode hwmon numbers or device paths that change between reboots.
Module structure¶
src/
+-- main.rs Entry point, arg parsing, terminal init
+-- app.rs App state, tick handler, event dispatch
+-- event.rs Keyboard/resize event handling
+-- config.rs TOML config loading and validation
+-- stress.rs CPU stress test worker management
+-- board/ Board detection and hardware profiles
| +-- mod.rs BoardProfile trait + detect() function
| +-- pi5.rs Pi 5 capabilities
| +-- pi4b.rs Pi 4B capabilities
| +-- zero2w.rs Zero 2W capabilities
+-- collectors/ Data collection modules
| +-- mod.rs Module exports
| +-- cpu.rs /proc/stat + cpufreq
| +-- memory.rs /proc/meminfo
| +-- thermal.rs thermal_zone + hwmon enumeration
| +-- network.rs /proc/net/dev
| +-- disk.rs /proc/diskstats + mount info
| +-- process.rs /proc/[pid]/ scanning
| +-- throttle.rs vcgencmd get_throttled
| +-- power.rs vcgencmd pmic_read_adc + measure_volts
| +-- fan.rs cooling_fan hwmon (Pi 5)
| +-- gpu.rs vcgencmd GPU metrics
| +-- pcie.rs /sys/bus/pci/devices/*/current_link_*
| +-- poe.rs /sys/class/power_supply/rpi-poe*
+-- ui/ TUI rendering
| +-- mod.rs Tab routing and layout framework
| +-- header.rs Top bar: board name, time, throttle
| +-- overview.rs Tab 1: dashboard gauges + sparklines
| +-- processes.rs Tab 2: sortable process table
| +-- power.rs Tab 3: PMIC, voltages, PCIe, PoE
| +-- network.rs Tab 4: interfaces + throughput
| +-- disk.rs Tab 5: partitions + I/O
| +-- system.rs Tab 6: board info, uptime, kernel
| +-- help.rs Help overlay popup
| +-- theme.rs Color themes
| +-- widgets/ Custom widget helpers
+-- util/
+-- ring_buffer.rs Fixed-size circular buffer
+-- format.rs Human-readable bytes, temps, watts
+-- vcgencmd.rs Async subprocess wrapper with caching
+-- sysfs.rs Helper for reading/parsing sysfs files
Application lifecycle¶
Startup¶
- Parse CLI arguments via
clap - Load config from
~/.config/pitop/config.toml(or CLI-specified path) - Detect board type from
/proc/device-tree/compatible - Create
Appstruct with all collectors and ring buffers - Set initial theme and starting tab
- Initialize terminal (raw mode, alternate screen)
- Start the tick loop
Tick loop¶
loop {
render UI
poll for keyboard events (with timeout = tick_rate)
drain any remaining queued events
if should_quit: break
if tick interval elapsed:
run always-on collectors (CPU, memory, thermal, network, fan, GPU, throttle)
run tab-specific collectors (based on active_tab)
update sparkline ring buffers
}
Shutdown¶
- Signal stress test workers to stop (if active)
- Restore terminal (raw mode off, alternate screen off, mouse capture off)
- Exit
Collector pattern¶
Each collector is a struct that owns its parsing state and provides a collect() method:
pub struct CpuCollector {
root: PathBuf,
prev_stats: Vec<CpuStat>, // previous tick's values for delta
}
impl CpuCollector {
pub fn new(root: &Path) -> Self { ... }
pub fn collect(&mut self, data: &mut CpuData) -> Result<()> { ... }
}
Key characteristics:
- Stateful: collectors keep previous-tick data for computing deltas (CPU usage, network throughput, disk I/O)
- Root-relative: all sysfs/procfs paths are relative to a configurable root, enabling testing with fixture directories
- Error propagation:
collect()returnsResult<()>. Errors are logged in verbose mode but never crash the app
Board detection¶
The board module reads /proc/device-tree/compatible at startup:
pub fn detect(root: &Path) -> BoardType {
// Read null-separated strings from /proc/device-tree/compatible
// Match against known SoC identifiers
// Return BoardType::Pi5, Pi4B, Zero2W, or Unknown
}
Each BoardType has a corresponding BoardProfile that defines:
- Human-readable name
- Available thermal zones
- Voltage source type (
Pmic,MeasureVolts,None) - Whether fan, PCIe, PoE monitoring is supported
Lazy tab refresh¶
To minimize CPU usage (important on the Pi Zero 2W), expensive collectors only run when their tab is active:
| Tab | Always-on | Tab-specific |
|---|---|---|
| Overview | CPU, memory, thermal, network, fan, GPU, throttle | -- |
| Processes | (same) | Process table scan |
| Power | (same) | PMIC/voltage, PCIe, PoE |
| Network | (same) | (network already always-on) |
| Disk | (same) | Disk partitions & I/O |
| System | (same) | (static data, no refresh) |
Ring buffers¶
Sparkline history is stored in fixed-size ring buffers (RingBuffer<T>):
- Default capacity: 60 samples (configurable via
history_size) - Valid range: 10 to 600 samples
- When full, the oldest sample is overwritten
- The
as_vec()method returns samples in chronological order for rendering
Separate ring buffers exist for CPU usage, memory usage, temperature, power draw, GPU frequency, and per-interface network throughput.
vcgencmd wrapper¶
All vcgencmd subprocess calls go through VcgencmdRunner:
- Uses
tokio::process::Command(async, never blocks the tick loop) - 2-second timeout per call
- 1-second minimum cache TTL (avoids re-running the same command within 1 second)
- Returns
Option<String>--Noneif vcgencmd is missing, times out, or fails - Never panics
Theme system¶
The Theme struct holds all UI colors. Three built-in themes are available (default, monochrome, solarized), plus a user-defined custom theme loaded from the config file.
All UI rendering modules read colors from the active Theme instance rather than hardcoding Color::* values. Theme cycling at runtime updates the Theme on the App struct.
Technology stack¶
| Dependency | Purpose |
|---|---|
ratatui 0.29 |
TUI widget library |
crossterm 0.28 |
Terminal backend (raw mode, events, alternate screen) |
tokio 1.x |
Async runtime (multi-thread, timers, process, fs) |
clap 4 |
CLI argument parsing (derive macros) |
anyhow 1 |
Error handling |
serde + toml |
Config file parsing |
libc |
Process kill (SIGTERM) |
Release profile¶
The release binary is optimized for size, which is important for the Pi Zero 2W: