Skip to content
tahuffman1sPublic

About

Sensor monitor written in rust

Resources

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

rsensor

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

Features

  • CPU — usage, per-thread load strip, boost clock, package power, and package plus per-die temperatures (Tctl, Tccd1…Tccd8, Intel Package/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-alert to 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 --help so 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.

Usage

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.

Alerts

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 stopped

Two 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.

Rates, and rules that only apply under load

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 down

A 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.

What happens when one fires

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".

Configuration

~/.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 = true

A [[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.

Scripting and export

--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.6

A 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 10s

CSV 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.

Watching another machine

# 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 --daemon

Prefer 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.

Records

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.

How it reads sensors

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.

A note on GPU throttling

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.

A note on per-process GPU use

--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.

A note on CPU package power

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.

Installation

From source

git clone https://github.com/tahuffman1s/rsensor.git
cd rsensor
cargo build --release
./target/release/rsensor

Or 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 only

The 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.fish

To 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 rsensor

It 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.

Arch Linux

makepkg -si

The 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.

Flatpak

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.rsensor

Note that rsensor is a terminal application, so the menu entry only works if your desktop has a terminal handler configured.

Development

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_layout

The 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.

License

MIT — see LICENSE.md.

About

Sensor monitor written in rust

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages