A lightweight terminal system monitor for CPU, memory and GPU sensors, inspired by psensor.
Rsensor
┌ CPU ───────────────────────────────────────────────────────────────────────────────────────────────────────┐
│AMD Ryzen 7 7700X 8-Core Processor 8 cores / 16 threads 4032 MHz power needs root │
│System load 0.69 0.88 0.84 · 1039 tasks · up 1 day 9 h │
│Usage ▏▋ ▏ 1.2% min 0.8% max 1.2%│
│ ▁ │
│Tctl ▏██████████████████▉ ▏ 39.2 °C min 39.2 °C max 39.2 °C│
│Tccd1 ▏██████████████ ▏ 29.1 °C min 29.1 °C max 30.8 °C│
│Threads ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ peak 5% │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Memory ───────────────────────────┐┌ Radeon RX 7700 XT / 7800 XT ─────┐┌ AMD · Raphael ────────────────────┐
│RAM ▏████████▏ ▏ 47.7% ││Usage ▏ ▏ 0.0% ││Usage ▏ ▏ 0.0% │
│Used 14.54 GiB / 30.49 GiB ││ ││ │
│███ ││Edge ▏████▊ ▏ 32.0 °C││Edge ▏█████▋ ▏ 35.0 °C│
│Range 15 GiB – 15 GiB ││Junction ▏█████▉ ▏ 39.0 °C││VRAM ▏▌ ▏ 3.1% │
│Free 15.95 GiB ││Memory ▏███████▌ ▏ 50.0 °C││Board 41 W · 600 MHz │
│Swap ▏ ▏ 0.0% 0 B││VRAM ▏█ ▏ 6.1% ││ │
│ ││Board 31 W · 0 MHz · fan 0 rpm││ │
│ ││Limit hotspot ││ │
└───────────────────────────────────┘└──────────────────────────────────┘└───────────────────────────────────┘
┌ Cores ─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│Core 0 ▏▍ ▏ 1.6% ▁▁ 29.1 °C Core 1 ▏▏ ▏ 0.8% ▁▁ 29.1 °C │
│Core 2 ▏▍ ▏ 1.7% ▁▁ 29.1 °C Core 3 ▏▍ ▏ 1.7% ▁▁ 29.1 °C │
│Core 4 ▏▏ ▏ 0.8% ▁▁ 29.1 °C Core 5 ▏▍ ▏ 1.6% ▁▁ 29.1 °C │
│Core 6 ▏▍ ▏ 1.7% ▁▁ 29.1 °C Core 7 ▏▉ ▏ 4.1% ▁▁ 29.1 °C │
│Tccd1 covers Core 0–7 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Disks ─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│DEVICE READ WRITE BUSY │
│nvme0n1 12.4 MiB/s 1.1 MiB/s 18.2% │
│nvme1n1 0 B/s 0 B/s 0.0% │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Network ───────────────────────────────────────────────────────────────────────────────────────────────────┐
│INTERFACE RX TX LINK │
│enp10s0 1.8 MiB/s 94 KiB/s 2.5 Gb/s │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌ Sensors 1–3/4 ─────────────────────────────────────────────────────────────────────────────────────────────┐
│acpitz temp1 16.8 °C gigabyte_wmi temp1 26.0 °C gigabyte_wmi temp2 32.0 °C │
│gigabyte_wmi temp3 39.0 °C gigabyte_wmi temp4 30.0 °C gigabyte_wmi temp5 34.0 °C │
│gigabyte_wmi temp6 35.0 °C nvme Composi…3X0C-00SJG0 33.9 °C nvme Composite nvme1 38.9 °C │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
every 500 ms q quit · r reset · c cores · s sensors · t procs · d disks · n net · f °F · / filter · ? help
- CPU — usage, per-thread load strip, boost clock, package power, and
package plus per-die temperatures (
Tctl,Tccd1…Tccd8, IntelPackage/Core N) - Per core — every physical core's load and temperature side by side, with a glyph per thread so a pinned single-threaded job reads as one hot sibling rather than a half-idle core
- Throttling — whether the part is losing clock speed, which is the question behind every temperature reading, from Intel's throttle counters and the kernel's frequency ceiling
- Memory — RAM and swap usage with observed minimum and maximum
- Load and uptime — one line, because neither earns a panel and together they say whether the machine is busy and how long it has been up
- GPU — AMD via sysfs and NVIDIA via NVML: load, edge/junction/memory temperatures, VRAM, board power, clocks, fan speed, and what the card says is limiting it
- Board sensors — NVMe drives, chipset, VRM, chassis fans, voltage rails, board power and current: anything hwmon exposes, each carrying its own unit
- Disks and network — throughput per block device and per interface, so the drive whose temperature you are watching can also say what it is doing
- Battery — charge, rate of drain in watts, time remaining, cycle count and the health figure that explains a laptop which no longer lasts an afternoon
- Processes — the busiest processes, because the question after "why is it 90 °C" is always "what is doing that" — optionally with per-process GPU use and VRAM, from DRM fdinfo and NVML
- Alerts —
--alert 'Tctl>85 for 30s', and rules that catch what a bare threshold cannot: a rate (Tctl rising 15 in 60s) and a guard (fan1 < 300 while Tctl > 60). A desktop notification when one fires, and--on-alertto run a command, which is the only outbound action that exists under--daemon - Records — the peak each sensor reached, kept across restarts and aged out of a rolling window, plus a downsampled trace that remembers the day
- Export — JSON, CSV and Prometheus output, continuous logging to a file, an HTTP endpoint with optional bearer-token authentication, and a client for watching another machine
- Trend sparklines, and readings coloured against each sensor's own critical threshold rather than a fixed guess — with every colour and threshold configurable
- Mouse and keyboard: scroll a long sensor list, click a row to open it
- A man page and shell completions, generated from the same definition as
--helpso they cannot drift from it - Sensors are read off the render loop and no subprocess is spawned while running: about 17 MB resident and 0.3% of one core at the default refresh, rising to 19 MB and 0.7% with every panel switched on at 250 ms
Temperatures are routed by the kernel's own hwmon labels, so multi-CCD Ryzen
parts, zenpower, Intel coretemp, ARM cpu_thermal and laptop acpitz all
work without per-chip special cases.
Threads are grouped onto their physical core from
/sys/devices/system/cpu/*/topology, which also gives each core the sensor
that measures it: Intel's coretemp exposes one per core and joins on the
kernel's core id, while AMD reports one per core-complex die that every core on
that die shares. A shared reading says so — Tccd1 covers Core 0–7 — rather
than presenting eight identical numbers as eight measurements.
rsensor [OPTIONS]
Display
-i, --interval <MS> Refresh interval in milliseconds [default: 500]
--gpu-interval <MS> GPU poll interval (defaults to the refresh interval)
--sensors <LIST> Board sensor kinds: temp, fan, voltage, power,
current [default: temp,fan,power]
--filter <PATTERN> Show only sensors whose name or key contains PATTERN
--procs Show the busiest processes
--proc-limit <N> Rows in the processes panel [default: 5]
--gpu-procs Attribute GPU use to processes (see the caveat below)
--disks Show block-device throughput
--disk-limit <N> Rows in the disks panel [default: 4]
--net Show network interface throughput
--net-limit <N> Rows in the network panel [default: 4]
-F, --fahrenheit Show temperatures in Fahrenheit
--no-gpu Do not monitor GPUs at all
--no-nvidia Skip NVIDIA/NVML probing, keep AMD monitoring
--no-sensors Hide the board sensors panel
--no-cores Hide the per-core panel
--no-procs Hide the processes panel
--no-disks Hide the disks panel
--no-net Hide the network panel
--no-battery Hide the battery panel on a machine that has one
--no-system Hide the load average and uptime line
--no-color Disable colour output (also honours NO_COLOR)
--no-mouse Leave the terminal its own click-to-select
Alerts
-a, --alert <RULE> Alert on a threshold, e.g. 'Tctl>85 for 30s'
--hysteresis <N> How far a reading must recover before clearing
--on-alert <CMD> Run a shell command when an alert changes state
--no-notify Do not send desktop notifications
Output
-1, --once Print one snapshot and exit
-f, --format <FORMAT> text, json, csv or prometheus (implies --once)
--log <PATH> Append every snapshot to a file
--log-format <FMT> csv or json [default: csv]
--log-interval <DUR> How often to append, e.g. 10s, 5m
Network
--serve <ADDR> Publish readings over HTTP, e.g. 9090 or 0.0.0.0:9090
--serve-token <TOK> Require this bearer token [env: RSENSOR_TOKEN]
--daemon Serve without drawing a UI
--connect <ADDR> Watch another machine running `rsensor --serve`
--connect-token <T> Bearer token [env: RSENSOR_CONNECT_TOKEN]
Records and configuration
--retention <DAYS> How long a record stands [default: 7, 0 for forever]
--history-path <P> Where to keep records
--no-history Keep no records
--reset-history Forget every record and exit
--config <PATH> Read this config file instead of the default
--no-config Ignore the config file entirely
Generated files
--man Write the man page to stdout and exit
--completions <SH> Write a completion script for SH to stdout and exit
(bash, zsh, fish, elvish, powershell)
Keys:
| Key | Action |
|---|---|
q, Ctrl+C |
quit |
Esc |
close what is open, then quit |
r |
reset the recorded minimum and maximum |
c |
show or hide the per-core panel |
s |
show or hide the board sensors panel |
t |
show or hide the processes panel |
m |
rank processes by CPU or by memory |
d |
show or hide the disks panel |
n |
show or hide the network panel |
b |
show or hide the battery panel |
f |
Celsius or Fahrenheit |
p, Space |
pause sampling |
/ |
filter the sensors panel |
↑ ↓, j k |
select a sensor |
PgUp PgDn |
scroll the sensors panel |
Home End |
first or last sensor row |
Enter |
open the selected sensor |
?, h |
key reference |
The mouse works too, unless --no-mouse is given: the wheel scrolls the sensors
panel, a click selects a sensor and a second click opens it, and a click on the
processes panel changes what it is ranked by. Mouse reporting takes the
terminal's own click-to-select away, which is the reason there is a switch.
The sensors panel scrolls when it does not fit, and its title says how much
is off screen — Sensors 4–9/24. It is the one panel that has to: a board can
publish forty channels and every one of them is worth reaching. The others are
ranked lists with a limit you set (--proc-limit, --disk-limit,
--net-limit), so on a short screen they shed from the bottom, which drops the
idle tail rather than anything you were looking for.
The interval is clamped to 200 ms at the low end, which is the shortest gap over which CPU usage means anything.
A rule names a sensor, a comparison and a threshold. The sensor can be a label
(Tctl), a metric key (cpu.temp.Tctl) or a substring that selects several at
once (nvme watches every drive, each firing and clearing on its own).
rsensor --alert 'Tctl > 85' # fires as soon as it crosses
rsensor --alert 'Tctl > 85 for 30s' # only if it stays there
rsensor --alert 'nvme > 70 repeat 10m' # nags while it is still hot
rsensor --alert 'fan1 < 300 for 10s' # a fan that has stoppedTwo details keep alerts from becoming noise. for requires the condition to
hold before anything fires, so a 200 ms spike during a compile is not an
incident; and clearing takes a margin below the threshold (--hysteresis,
2 units by default) so a sensor sitting exactly on the line does not flap.
A threshold catches a reading that has arrived somewhere bad. Two failures it cannot catch have their own grammar.
rsensor --alert 'Tctl rising 15 in 60s' # a climb, before it crosses
rsensor --alert 'fan1 falling 500 in 30s' # a fan slowing downA failing pump, a loose heatsink and a dying fan bearing all show up as a fast
climb long before any absolute limit is crossed — by which point the only
remaining option is a shutdown. A rate rule needs a refresh interval at least as
fast as its window; over a coarser --interval the movement is scaled to the
window it asked about and the rule will not fire.
rsensor --alert 'fan1 < 300 while Tctl > 60'A fan below 300 rpm is unremarkable on an idle desk and an emergency under load. Without the guard that rule fires every night the machine sits there doing nothing, which teaches whoever reads it to ignore it — and an alert nobody reads is worse than no alert, because it also hides the one that matters. The guard holds when any metric it selects satisfies it, and a rule that is already firing clears when its guard stops holding.
for, repeat and while may be written in any order.
A firing alert takes a banner across the top of the screen and sends one desktop
notification, through notify-send where libnotify is installed and glib's
gdbus otherwise. Nothing is spawned on a normal tick — only when an alert
actually changes state. --no-notify leaves the banner and drops the
notifications.
--on-alert runs a shell command instead, which is what makes rsensor a
watchdog rather than only a display — and the only outbound action that exists
under --daemon, where there is no screen to watch and no desktop to notify.
rsensor --alert 'Tctl > 90 for 60s' \
--on-alert 'curl -sf -X POST "$WEBHOOK" -d "$RSENSOR_MESSAGE"'The event arrives in the environment, never as arguments — a sensor label comes from the kernel and would need escaping:
| Variable | Example |
|---|---|
RSENSOR_ALERT |
fired, repeated or cleared |
RSENSOR_KEY |
cpu.temp.Tctl |
RSENSOR_LABEL |
Tctl |
RSENSOR_VALUE |
91.5 |
RSENSOR_UNIT |
°C |
RSENSOR_RULE |
Tctl > 85 for 30s |
RSENSOR_MESSAGE |
Tctl is 91.5 °C (Tctl > 85 for 30s) |
RSENSOR_HOST |
workstation |
The command runs through sh -c, so a pipeline or a variable works without
rsensor implementing quoting. Output goes to /dev/null: a hook that printed
would corrupt the display. At most sixteen may be running at once — a hook that
blocks forever must not be the thing that takes the machine down — and anything
past that is dropped rather than forked.
A sensor that stops reporting does not clear its alert. The machine may be in trouble precisely because the sensor stopped answering.
Every rule is also exported, firing or not, as a 0/1 gauge in the alerts
group — so Prometheus can alert on rsensor's own rules, and can tell "recovered"
from "the exporter went away".
~/.config/rsensor/config.toml, or --config <path>. Everything in it is
optional, and every setting has a command-line equivalent that wins over it.
contrib/config.example.toml is the full
reference, with every setting and its default.
interval = 500
fahrenheit = false
mouse = true
sensors = ["temp", "fan", "power"]
[panels]
cores = true
sensors = true
system = true # the load average and uptime line
battery = true # only ever drawn on a machine that has one
procs = false
proc_limit = 5
gpu_procs = false # per-process GPU use; expensive, see below
disks = false
net = false
[alerts]
rules = [
"Tctl > 85 for 30s",
"Tctl rising 15 in 60s",
"fan1 < 300 while Tctl > 60",
]
notify = true
hysteresis = 2.0
on_alert = 'logger -t rsensor "$RSENSOR_MESSAGE"'
[history]
enable = true
retention_days = 7
[server]
listen = "127.0.0.1:9090"
token = "a-long-random-string"
# Colours and the thresholds they change at. A colour is an ANSI name, a 0–255
# index for a 256-colour terminal, or #rrggbb for truecolor.
[theme]
load_high = "#f7768e"
temp_warn_at = 0.65
animate = false # stop the title cycling colours
# A board that labels its sensors temp1..temp6 tells you nothing. Name the one
# that matters and hide the rest.
[[sensor]]
key = "gigabyte_wmi/temp3"
name = "VRM"
order = 1
[[sensor]]
key = "gigabyte_wmi"
hide = trueA [[sensor]] block matches by key, by label, or by any substring of either;
an exact match always beats a broad one, which is what lets the two blocks
above coexist. key is the stable identifier shown in a sensor's detail view
(press Enter on it) and in every export.
Hiding a sensor affects the screen only. It is still exported, still logged and still alerted on — dropping a reading from the JSON because a panel was crowded would be a lie.
An unrecognised setting is reported on stderr and ignored; a malformed one
names the file and line and stops the program, because silently ignoring a
typo'd alert rule is worse than refusing to start. A mistyped [theme] colour
or key is named along with what would have been accepted.
--once needs no terminal, so it is useful for logging:
$ rsensor --once --no-gpu
CPU AMD Ryzen 7 7700X 8-Core Processor (8 cores / 16 threads)
usage 19.1% (min 0.8%, max 19.1%)
clock 5464 MHz
power 88.4 W (peak 142.1 W)
Tctl 69.1 °C
Tccd1 78.2 °C
...--format gives the same snapshot in a machine format, and implies --once:
$ rsensor --format json | jq '.metrics[] | select(.key == "cpu.temp.Tctl")'
{
"key": "cpu.temp.Tctl",
"label": "Tctl",
"group": "cpu",
"unit": "°C",
"value": 64.6,
"limit": 95,
"min": 58,
"max": 64.6
}
$ rsensor --format csv > sample.csv
$ rsensor --format prometheus | head -3
# HELP rsensor_temperature_celsius Reading from a hardware sensor.
# TYPE rsensor_temperature_celsius gauge
rsensor_temperature_celsius{host="workstation",group="cpu",sensor="Tctl",key="cpu.temp.Tctl"} 64.6A reading that is unavailable is null in JSON and an empty CSV cell — never
zero. That distinction is kept everywhere: a sensor that is not answering is
not a sensor reading zero.
Every reading is exported, including the ones no panel is currently showing:
load average and uptime, disk and network throughput, battery charge and health,
whether the CPU is throttling, and one 0/1 gauge per alert rule. Prometheus
gets base SI units as the convention requires, so a throughput becomes
rsensor_bytes_per_second and a percentage becomes a ratio.
To log continuously, --log appends while rsensor runs:
rsensor --log ~/thermals.csv --log-interval 10sCSV columns are fixed when the log is opened, and reopening an existing log reuses the columns its header already promised, so a run with a GPU removed leaves those columns empty rather than shifting everything left.
# On the machine with the sensors
rsensor --serve 9090 --daemon
# Anywhere else
rsensor --connect other-host:9090--serve publishes four endpoints:
| Path | What it serves |
|---|---|
/ |
a list of the endpoints |
/metrics |
Prometheus text format |
/metrics.json |
every reading as JSON |
/snapshot |
the tab-separated format --connect reads |
--serve alongside the normal UI publishes while you watch; --daemon runs
without drawing anything, for a systemd unit or a container.
contrib/rsensor.service is a hardened unit for
exactly that, with its settings in
contrib/rsensor.default.
A bare port binds to loopback only. Publishing every sensor on the machine
to the network should take saying which interface (--serve 0.0.0.0:9090).
--serve-token requires Authorization: Bearer <token> — or ?token=…, which
is the form a browser address bar can manage — on every endpoint but the index.
The index stays open deliberately: it lists endpoint names and no readings, and
a 401 there makes a working deployment look dead. The token is compared in
constant time and never appears in a response. --connect-token is the other
end of it.
RSENSOR_TOKEN=$(openssl rand -hex 32) rsensor --serve 0.0.0.0:9090 --daemonPrefer the environment variable to the flag: an argument is visible to every
user on the machine through ps. --serve-token wins over RSENSOR_TOKEN,
which wins over [server] token in the config file. The systemd unit reads the
environment for this reason — note that systemd does not expand a variable
inside another variable's value, so putting --serve-token "$RSENSOR_TOKEN" in
RSENSOR_ARGS would pass those fourteen literal characters as your token.
Without a token there is no authentication of any kind, so if you publish to the network, either set one or put something that authenticates in front of it.
--daemon needs an address, which may come from --serve or from
[server] listen in the config file.
For Prometheus, point a scrape job at /metrics. Values are converted to base
SI units as the convention requires: percentages become ratios and megahertz
become hertz.
rsensor keeps the highest and lowest each sensor reached, with the time, in
$XDG_STATE_HOME/rsensor/records.tsv. They survive a restart and age out of a
rolling seven-day window, so the figure means "this week" rather than "since I
installed it".
Records show in a sensor's detail view (Enter), in the --once report, and
nowhere else. r resets the session's minimum and maximum and deliberately
leaves the records alone; --reset-history clears them, and --no-history
keeps none.
The detail view also carries a second trace. The sparkline everywhere else is
256 raw samples — about two minutes at the default tick, which answers "what is
it doing" but never "when did that happen". Alongside it, each sensor's readings
are folded into one bucket a minute (its minimum, maximum and mean) and kept for
a day, so a spike during last night's compile is still on screen. A minute that
recorded nothing is drawn as a gap rather than a trough: the machine was asleep,
and a dip that never happened would be a lie about the shape of the day. That
trace is not cleared by r, which is the whole point of it.
| Source | What it provides |
|---|---|
/sys/class/hwmon |
every temperature, fan, voltage, power and current channel |
sysinfo |
CPU usage and clocks, memory, and the process table |
/sys/devices/system/cpu/*/topology |
which threads share a physical core |
/sys/devices/system/cpu/*/thermal_throttle |
Intel throttle counters |
/sys/devices/system/cpu/*/cpufreq |
the frequency ceiling currently in force |
/sys/class/powercap |
CPU package power, differenced from the energy counter |
/proc/loadavg, /proc/uptime |
load average, task count, uptime |
/proc/diskstats, /sys/block |
block-device throughput and busy time |
/proc/net/dev, /sys/class/net |
interface throughput and link speed |
/sys/class/power_supply |
battery charge, draw, health, cycles; mains state |
/sys/class/drm/card*/device |
AMD load, VRAM, power, clocks, fan, temperatures |
…/device/gpu_metrics |
what an AMD card says is limiting its clocks |
/proc/*/fdinfo |
per-process GPU engine time and VRAM (AMD) |
NVML (libnvidia-ml) |
NVIDIA load, temperatures, VRAM, power, clocks, fan, throttle reasons |
/usr/share/hwdata/pci.ids |
GPU model names |
Everything except /proc/*/fdinfo is a handful of small reads per tick.
Every hwmon path is resolved once, at startup, by reading the kernel's own labels; polling then reads only those exact files.
libnvidia-ml is loaded at runtime, so a build with NVIDIA support still runs
fine on machines with no NVIDIA driver — it simply reports no NVIDIA devices.
GPU model names come from the PCI ID database rather than lspci, which keeps
naming working inside the Flatpak sandbox; lspci is only a fallback.
Each GPU is tracked by its PCI slot, so two identical cards keep separate histories. Two identical drives are separated the same way: when the model name no longer tells them apart, the name falls back to the device the chip hangs off.
Both vendors publish why a card is holding its clocks down, and neither means it
as an alarm. On an RDNA3 card the SMU sets its hotspot-temperature bit whenever
the card has any work at all — at 37 °C, idle — because it is naming the
constraint that is currently binding, not reporting distress; NVML has an
explicit GpuIdle reason for the same phenomenon. A badge wired to "throttle
status is non-zero" would therefore be lit permanently on an idle desktop, which
is worse than showing nothing.
So the GPU panel reports it as Limit, in the card's own vocabulary, and only a
reason that means the hardware is protecting itself — VRHOT, PROCHOT,
NVIDIA's hardware slowdowns — is coloured as a warning. AMD's throttle status is
only decoded for gpu_metrics revisions whose layout has been verified against
real hardware; an unrecognised revision reports nothing rather than decoding
whichever field happens to sit at that offset.
The CPU's throttling is a different matter and is treated as one: Intel's counters are cumulative, so what the CPU header reports is that they advanced, and for five minutes afterwards that it recently did.
--gpu-procs is off by default for two reasons. It reads every open file
descriptor of every process, which dwarfs everything else rsensor does; and
/proc/<pid>/fdinfo is readable only by a process's owner, so a compositor
running as another user is simply invisible. When it is on and nothing is
visible, the panel says so rather than showing an empty column.
Most kernels carry the mitigation for CVE-2020-8694, which makes the powercap
energy counter readable only by root — so power needs root in the CPU header
is the normal state of affairs, not a bug. Where a chip publishes power through
hwmon instead (zenpower, amd_energy), that reading is preferred and needs
no privileges. GPU board power is unaffected either way.
git clone https://github.com/tahuffman1s/rsensor.git
cd rsensor
cargo build --release
./target/release/rsensorOr install it straight onto your path:
cargo install --path .Requires Rust 1.95 or newer, which is what sysinfo and ratatui ask for. Two optional features are on by default: nvidia
pulls in NVML, and gen pulls in the man-page and completion generators. Drop
either or both:
cargo build --release --no-default-features # neither
cargo build --release --no-default-features -F nvidia # NVML onlyThe man page and shell completions are generated by the binary itself, from the
same definition --help comes from, so they cannot drift from it:
rsensor --man > rsensor.1
rsensor --completions bash > /usr/share/bash-completion/completions/rsensor
rsensor --completions zsh > /usr/share/zsh/site-functions/_rsensor
rsensor --completions fish > ~/.config/fish/completions/rsensor.fishTo publish readings with no terminal attached, contrib/rsensor.service is a
hardened systemd unit:
sudo install -Dm644 contrib/rsensor.service /etc/systemd/system/rsensor.service
sudo install -Dm644 contrib/rsensor.default /etc/default/rsensor
sudo systemctl enable --now rsensorIt runs under DynamicUser with no capabilities, which is enough for everything
hwmon publishes. The one casualty is the powercap energy counter, which most
kernels restrict to root — see the note above.
makepkg -siThe PKGBUILD builds the tagged release from GitHub and installs the binary,
the man page, completions for bash, zsh and fish, the systemd unit and the
example config. hwdata or pciutils gives GPUs their marketing names,
nvidia-utils enables NVIDIA monitoring, and libnotify provides
notify-send for alerts; all are optional.
The Flatpak build has no network access, so crates must be vendored first:
# One-off prerequisites
flatpak install org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08 \
org.freedesktop.Sdk.Extension.rust-stable//24.08
cargo vendor # populates ./vendor for the offline build
flatpak-builder --force-clean build-dir org.tahuffman1s.rsensor.yml
# Try it without installing
flatpak-builder --run build-dir org.tahuffman1s.rsensor.yml rsensor
# Or install locally
flatpak-builder --repo=repo --force-clean build-dir org.tahuffman1s.rsensor.yml
flatpak --user remote-add --no-gpg-verify rsensor-repo repo
flatpak --user install rsensor-repo org.tahuffman1s.rsensor
flatpak run org.tahuffman1s.rsensorNote that rsensor is a terminal application, so the menu entry only works if
your desktop has a terminal handler configured.
cargo test --all-features # unit and render tests
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --all --check
# Print a rendered frame to eyeball the layout
cargo test --all-features -- --nocapture preview_layoutThe layout is exercised against ratatui's test backend across a spread of
terminal sizes, so panels degrade predictably instead of vanishing on a short
screen. Sensor discovery is tested against fixture directories rather than the
host's hardware, so the suite means the same thing on every machine.
The dependency list is deliberately short: ratatui, clap, sysinfo, and —
behind feature flags — nvml-wrapper, clap_mangen and clap_complete. The
config file parser, the JSON and Prometheus output, the wire format and the HTTP
server are all a few hundred lines each rather than a dependency tree, which is
the point of a program that advertises itself as costing nothing to run.
Every sysfs and procfs reader is tested against a fixture tree, and each also has one test that runs against the real host and asserts only that it does not panic and that whatever it returns is plausible — so the suite is meaningful on a machine with none of the hardware, and still catches a kernel that has changed a format underneath it. The shipped example config is checked against the code too, so a setting it presents as a default cannot quietly stop being one.
MIT — see LICENSE.md.