Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

tuxctl

tuxctl is a lightweight, keyboard-first control-center TUI for Linux, written in Rust with Ratatui and Crossterm. One terminal window shows the system at a glance, the processes, systemd services, the journal and the network interfaces, and it stays cheap while it runs: about 0.6 % of one core and under 3 MiB of memory when idle (static build; loading NVML for an NVIDIA GPU adds about 20 MiB).

What it does

  • Overview: cards for CPU, GPU, memory, network, storage and pinned processes, with graphs, temperatures and power, colored by load.
  • Processes: a sortable, searchable process list with pinning and safe SIGTERM / SIGKILL, confirmed before anything is sent.
  • Services: systemd units and their states, filtered and searchable.
  • Logs: the systemd journal, followed live, filtered by priority.
  • Network: interfaces with live rates, addresses and counters.

Every screen works with the keyboard; the mouse can switch tabs, select rows, sort, scroll and press buttons.

Principles

  • Restrained: a readable interface rather than as much as possible on screen.
  • Cheap while idle: data is collected in the background at a chosen interval, and the screen is redrawn only when something visible changed.
  • Safe: text from other users can never reach the terminal as escape sequences, and signals go only to the exact process that was confirmed.
  • Linux-native: it reads /proc, /sys, systemctl and journalctl directly and asks for no privileges.

Start with Installation, then the Controls.

Installation

Requirements

  • Linux. tuxctl uses Linux interfaces such as /proc and /sys directly.
  • systemctl for the Services screen and journalctl for the Logs screen.
  • Rust 1.88 or newer to build from source.

Process signals use Linux pidfds so that a signal can never reach a process that reused the PID. If pidfds are unavailable, tuxctl refuses to send the signal rather than fall back to PID-only signaling.

Prebuilt binaries

Each release has statically linked (musl) binaries for x86_64 and aarch64. They do not depend on the system C library, so they run on any Linux distribution. Download one, verify it, and install it:

arch=$(uname -m)   # x86_64 or aarch64
base=https://github.com/Seqat/tuxctl/releases/latest/download
curl -LO "$base/tuxctl-$arch-unknown-linux-musl.tar.gz" -LO "$base/SHA256SUMS"
sha256sum -c --ignore-missing SHA256SUMS
tar xzf "tuxctl-$arch-unknown-linux-musl.tar.gz"
install -Dm755 "tuxctl-$arch-unknown-linux-musl/tuxctl" ~/.local/bin/tuxctl

~/.local/bin must be on your PATH.

From v0.3.4 on, each archive also has a build provenance attestation. With the GitHub CLI, this confirms that the archive was built by the project’s release workflow from its repository:

gh attestation verify "tuxctl-$arch-unknown-linux-musl.tar.gz" --repo Seqat/tuxctl

NVIDIA GPUs: the glibc binary

The static binaries cannot load NVIDIA’s NVML library, so they show no temperature or usage for NVIDIA GPUs on the proprietary driver. From v0.3.4 on, each release also has an x86_64 binary linked against glibc, which can. It needs glibc 2.28 or newer (RHEL 8, Debian 10, Ubuntu 20.04, and later releases of most other distributions); ldd --version shows yours.

base=https://github.com/Seqat/tuxctl/releases/latest/download
curl -LO "$base/tuxctl-x86_64-unknown-linux-gnu.tar.gz" -LO "$base/SHA256SUMS"
sha256sum -c --ignore-missing SHA256SUMS
tar xzf tuxctl-x86_64-unknown-linux-gnu.tar.gz
install -Dm755 tuxctl-x86_64-unknown-linux-gnu/tuxctl ~/.local/bin/tuxctl
gh attestation verify tuxctl-x86_64-unknown-linux-gnu.tar.gz --repo Seqat/tuxctl

On aarch64, or with an older glibc, build from source instead (see NVIDIA GPUs).

From crates.io

cargo install tuxctl --locked

This builds against the system’s glibc, so it can load NVML and show NVIDIA GPUs on the proprietary driver.

From the repository

A tagged version straight from GitHub:

cargo install --git https://github.com/Seqat/tuxctl --tag v0.3.4 --locked

Or from a clone:

git clone https://github.com/Seqat/tuxctl.git
cd tuxctl
cargo install --path . --locked

To build an optimized binary without installing it:

cargo build --release --locked
./target/release/tuxctl

First run

tuxctl            # start on the Overview
tuxctl --check    # list the sensors tuxctl finds, then exit

Everything works without setup. A few sensors need a step on some machines; tuxctl --check says which, and Optional setup explains each one.

Command-line options

tuxctl [--interval <DURATION>] [--no-nvidia-temperature] [--check]
OptionDescription
--interval <DURATION>Sampling interval for CPU, memory, and network: 250ms, 500ms, 1s (default), 2s, 5s, 10s, 30s, or 60s. Processes refresh at most once per second and services at most every 5 seconds. + and - change the interval while tuxctl runs.
--no-nvidia-temperatureDo not load NVML for NVIDIA GPUs on the proprietary driver. NVML shows their temperature and usage but adds about 20 MiB of private memory (see Temperatures and GPUs); static builds never load it. The Help overlay shows whether it is on.
--checkPrint which sensors tuxctl finds on this machine, their values and origins, and what would enable the missing ones; then exit. See Optional setup.
-h, --helpPrint help.
-V, --versionPrint the version.

A --check report looks like this:

tuxctl 0.3.4 sensor check

CPU     AMD Ryzen 5 7500F 6-Core Processor
        temperature  58°C       k10temp Tctl
        power        readable   from RAPL

GPU     NVIDIA GeForce RTX 5070 Ti  (nvidia)
        temperature  40°C       NVML
        usage        util 0%, VRAM 1.7/15.9 GiB, 27 W, fan 0%  NVML

Disk    NVMe0  WD Blue SN5100 1TB  (nvme0n1)
        temperature  41°C       nvme Composite

Controls

Every action has a key; the mouse offers the common ones too. ? shows the keys for the current screen inside tuxctl.

Global Controls

KeyAction
1 - 5Switch directly to tab: Overview, Processes, Services, Logs, Network
Tab / Shift+TabNext / previous tab
→ / ←Next / previous tab
?Toggle Help dialog
+ / -Longer / shorter sampling interval (250ms to 60s, shown as ⟳ in the top-right corner)
EscDismiss dialog / clear the search, then the view filter / open the main menu
qOpen the main menu on Exit; Enter or q again quits
Ctrl+CQuit immediately, from anywhere
KeyAction
↑ / kMove selection up
↓ / jMove selection down
PageUp / PageDownMove selection by page
Home / EndJump to first / last item
/Begin search / filter; ↑ / ↓ and PageUp / PageDown move through the matches while typing
EnterOpen detailed inspection

Processes

KeyAction
cSort by CPU %
mSort by Memory
pSort by PID
nSort by Name
T / Shift+TRequest SIGTERM for selected process
K / Shift+KRequest SIGKILL for selected process
P / Shift+PPin / unpin the selected process (up to 8)
Shift+↑ / Shift+↓Move the selected pinned process up / down (Alt+↑ / Alt+↓ also work)
vHide / show kernel threads

Repeated sort commands toggle the sort direction. Pinned processes stay at the top in the order you give them, marked with *; sorting applies to the rows below them. While a search is active, pinned processes that do not match stay visible but dimmed. A pinned process that exits is shown as exited for a few seconds and then removed; it can never be signaled.

Signal Confirmation

KeyAction
Tab / ← / → / h / lMove focus between Cancel and confirmation
EnterExecute focused action
EscCancel and close

Esc opens the main menu when there is no dialog, search, or view filter to clear.

KeyAction
↑ / ↓Move between About and Exit (k / j also work)
EnterOpen About, or exit tuxctl
qExit tuxctl
EscClose the menu (from About, go back to the menu)

Services

KeyAction
rRequest an immediate service refresh
vCycle the view: all units → loaded units (hide not-found) → failed units

Logs

KeyAction
fToggle follow mode
SpacePause / resume
vCycle the minimum priority: all → notice → warning → error

Mouse Controls

  • Tabs: Click a tab title to switch screens.
  • Selection: Click rows in Processes, Services, Logs, or Network.
  • Scrolling: Use the mouse wheel over list/table areas.
  • Process sorting: Click PID, NAME, CPU, or MEMORY headers.
  • Pinned processes: Click ▲ / ▼ at the end of a pinned row to move it (shown when there are at least two pins and the terminal is wide enough).
  • Confirmation dialogs: Click Cancel or the confirmation action.
  • Main menu: Click About or Exit.
  • Sampling interval: Click [-] / [+] next to ⟳ in the top-right corner.

Overview

The first screen: the whole system at a glance, as a set of cards that uses the full terminal.

Summary line

The top border shows the hostname, kernel, uptime, and the process, running and zombie counts (zombies are highlighted when there are any). On a narrow terminal the kernel goes first, then the counts shorten to 397p · 2r · 0z, then the uptime goes.

Cards

Each card names its component in the title, with the model, temperature and power where known: GPU RTX 5070 Ti · 43°C · 28W. When the title does not fit, the model is shortened first.

  • CPU: a graph of total utilization, utilization and the 1/5/15-minute load averages, and a grid of every logical CPU. Package power appears where this user can read it (see CPU power).
  • GPU: the discrete GPU (or the only one) with a utilization graph, utilization, VRAM use and fan speed where the driver reports them, and a row for each other GPU. See Temperatures and GPUs.
  • Memory: a graph of RAM use, RAM and swap gauges, and the RAM modules when EDAC reports them.
  • Network: the main physical interface with its state and temperature, a graph of its traffic with the peak, and a row for each other interface. The graph is logarithmic, so one spike does not flatten everyday traffic; traffic below 1 KiB/s stays at the baseline.
  • Storage: usage of every local filesystem (one line per device, so btrfs subvolumes appear once; network, FUSE and loop mounts are left out), and the NVMe, SATA and SCSI disks with their temperature and read/write throughput.
  • Pinned: the processes pinned with P on the Processes screen, with their CPU and memory. A pinned process that exits stays for a few seconds, dimmed, as exited.

Graphs keep the last 240 samples and show as many as fit the card; the bottom border states the time span shown. They start over when the sampling interval changes.

Temperatures, GPU usage, CPU power and filesystem use are read only while the Overview is visible; the other screens keep the last values.

Colors

Utilization values (total and per-CPU, RAM, GPU utilization and VRAM), pinned processes’ CPU, and each column of the CPU, memory and GPU graphs take a band:

ValueColor
below 10 %light blue
10 % to 65 %green
65 % to 80 %yellow
80 % to 95 %orange
95 % and abovered

A process busy on several cores counts as 100 %. The network graph shows throughput rather than a percentage and stays neutral. Temperatures use the same bands as a share of their critical limit (see Temperatures and GPUs). A value is always shown as text, so the color never carries meaning alone.

tuxctl reads COLORTERM and TERM once at startup: truecolor or 24bit terminals get the full palette, *256color terminals the nearest 256-color entries, and anything else the 16 basic colors (where orange becomes bright red).

Layout

The cards arrange themselves by terminal width:

  • 150 columns and more: a 2×2 grid (CPU | GPU, Memory | Network), Storage below it, and Pinned as a column on the right.
  • 100 to 149 columns: the same grid, with Storage and Pinned side by side below it.
  • Narrower: the cards stacked in priority order (CPU, Memory, Pinned, Network, Storage, GPU), each graph in its card’s title row, or above the card’s rows when the terminal is tall enough for every card that way.

Cards side by side are equally wide, so their graphs span the same time. A short terminal gives up, in this order: the optional rows (the per-CPU grid, other GPUs and interfaces, memory modules), then graph height down to one row, then list rows (Pinned and Storage then say how many they leave out), and only then whole cards.

Processes

A live list of every process from /proc, with the total CPU and RAM use above it.

  • Columns: PID, name, CPU percentage and resident memory. Sort with c, m, p or n, or by clicking a column header; sorting again reverses the order.
  • Search: / filters by name, PID or command line, case-insensitively.
  • Kernel threads: v hides or shows them.
  • Details: Enter opens the selected process: its name, command line, state, parent PID, CPU and memory, and whether it is a kernel thread.

Pinning

P pins the selected process to the top of the list; up to 8 can be pinned, in your own order (Shift+↑ / Shift+↓, or the ▲ / ▼ controls at the end of a pinned row). Sorting applies to the rows below them. During a search, pinned processes that do not match stay visible but dimmed. Pinned processes also appear on the Overview.

A pin follows the process identity, (PID, start time), so a new process that reuses the PID never inherits it. A pinned process that exits is shown as exited for a few seconds and cannot be signaled.

Signals

T requests SIGTERM and K requests SIGKILL for the selected process. Nothing is sent until you confirm, and Cancel is the default.

When you confirm, tuxctl opens a pidfd for the process and checks that its start time still matches the one you saw before sending the signal through that pidfd. A process that exited, or a new process that took its PID, is never signaled. If pidfds are not available, the signal is refused.

Signaling other users’ or privileged processes follows the normal Linux permission rules.

Services

The systemd units from systemctl: unit, load state, active state, sub-state and description.

  • States: ● active, ✖ failed, ○ inactive, ◌ activating.
  • Search: / filters units, case-insensitively.
  • View: v cycles through all units, loaded units (hiding not-found), and failed units only.
  • Refresh: r asks for a new listing at once.
  • Details: Enter shows the selected unit. The screen is read-only; it never starts, stops or changes a unit.

Services are collected only while this screen is visible, and refreshed each time it is opened, at most every 5 seconds. A systemctl call that hangs is stopped after 10 seconds, so it cannot stall the screen or exit.

Logs

The systemd journal, streamed from journalctl. It starts the first time the Logs screen is opened, with the last 200 entries.

  • Time: entries show the local time (HH:MM:SS); the detail view has the full date and UTC offset.
  • Severity: errors, warnings, informational and debug messages are styled differently.
  • Follow and pause: f follows new entries; Space pauses and resumes.
  • Search: / filters the entries.
  • Priority: v cycles the minimum priority: all, notice, warning, error.
  • Details: Enter shows the whole, multi-line message.

The screen keeps the last 2000 entries. Under a burst of messages, entries that cannot be taken in time are counted as dropped rather than piling up, and reading the journal never holds up the keyboard.

Network

The network interfaces from /proc/net/dev and /sys/class/net.

  • Rates: live receive and transmit rates, computed from the time actually elapsed between samples, and the total counters.
  • Addresses: IPv4 and IPv6.
  • State: ● up, ○ down, ◌ dormant, or ◌ unknown (for example the loopback interface).
  • Details: Enter shows the MAC address, MTU, packet counts, errors and dropped packets.

Rates start over cleanly when an interface appears, disappears, or /proc/net/dev cannot be read for a moment, instead of showing a spike.

Temperatures and GPUs

Temperatures

A temperature appears next to a component only when the kernel provides a sensor for it; components without one show nothing. – means the sensor exists but has no value right now, for example while a GPU is runtime-suspended. Sensors are read at most every 2 seconds, whatever the sampling interval, and only while the Overview is visible.

ComponentSource
Intel CPUcoretemp: the Package id N sensor, or the hottest core when there is none
AMD CPUk10temp or zenpower: Tdie, else Tctl (per-CCD sensors are not used)
Other CPUs (ARM, SoCs)Only without a CPU hwmon driver: the hottest thermal zone whose type names the CPU or SoC (never acpitz)
AMD GPUamdgpu (the edge sensor) or radeon hwmon
Intel GPUi915 / xe hwmon, when the GPU has its own sensor; integrated GPUs usually do not
NVIDIA GPU, nouveaunouveau hwmon
NVIDIA GPU, proprietary driverNVML (libnvidia-ml.so.1, installed with the driver); not in the static release binaries
NVMenvme hwmon (Composite)
SATA / SASdrivetemp hwmon, only when that module is loaded (see Optional setup)
Network adapterA hwmon sensor on the adapter or on its PHY

RAM (SPD) sensors are not shown.

Colors

A temperature takes the color band (see Overview) of its share of the component’s critical temperature: the limit the driver reports (temp*_crit, else temp*_max; for NVIDIA GPUs the slowdown temperature from NVML), or, when it reports none, an assumed limit:

ComponentAssumed critical temperature
CPU95 °C
GPU95 °C
NVMe80 °C
SATA / SAS and other disks60 °C
Network adapter100 °C

The value is always shown, so the color never carries meaning alone.

GPU usage

DriverUtilizationVRAMPowerFan
NVIDIA proprietaryNVMLNVMLNVMLNVML
amdgpugpu_busy_percentmem_info_vram_*hwmonhwmon
nouveau––hwmonhwmon
Intel (i915, xe)––––

A part the driver does not report is left out rather than shown as a placeholder.

NVIDIA GPUs on the proprietary driver

NVIDIA’s proprietary driver has no hwmon sensors, so tuxctl reads these GPUs through NVML, the library the driver installs (libnvidia-ml.so.1). It loads NVML at run time, and only when it finds a GPU using the nvidia driver.

  • Memory: NVML adds about 20 MiB of private memory and a thread from the first reading on (on the reference machine: RssAnon +20.2 MiB, PSS +21.4 MiB; RSS +24.7 MiB including 4.5 MiB of shared library pages). nvmlShutdown does not free it, not even with RTD3, where NVML is shut down after every reading; closing the library does. tuxctl closes NVML once the Overview has been hidden for 10 seconds (checked at each sample, so up to about 2 minutes at a 60 s interval), and loads it again when the Overview is shown: the first reading then takes about 40 ms longer, on the background worker. Other monitors that read NVIDIA GPUs load the same library and pay the same cost.
  • CPU: utilization and power are read on every sample, temperature, VRAM and fan every 2 seconds. That adds about 0.35 % of one core at the default 1 s interval and about 0.65 % at 250 ms on the reference machine, and only while the Overview is visible.
  • Opting out: --no-nvidia-temperature leaves NVML unloaded.
  • Static binaries: the static release binaries cannot load NVML at all, so they show no temperature or usage for these GPUs; the GPU card says “NVML needs a glibc build” instead. Use a glibc build: the x86_64 tuxctl-x86_64-unknown-linux-gnu.tar.gz release binary, or cargo install tuxctl --locked.

Sleeping GPUs

tuxctl never wakes a sleeping GPU: it reads power/runtime_status first and shows – while the GPU is suspended.

NVML stays initialized only when the GPU cannot runtime-suspend anyway (power/control is on, or the driver reports Runtime D3 status as not supported or disabled). With RTD3 enabled, as on many hybrid laptops, NVML is initialized for each reading and shut down right after, and only while every NVIDIA GPU is awake, so tuxctl never keeps the GPU powered.

Optional setup

Everything works without setup. Three things need a step on some machines, and tuxctl --check shows which apply to yours: it lists the sensors tuxctl finds, with their values and where they come from, and says what would enable the missing ones.

To seeYou need
SATA / SAS disk temperaturesthe drivetemp kernel module (below)
CPU package powerreadable RAPL energy counters (below)
Temperature and usage of NVIDIA GPUs on the proprietary driverthe glibc release binary or a source build of tuxctl (below)

tuxctl itself never changes the system and asks for no privileges; each step below is one you take yourself, and each can be undone.

SATA and SAS disk temperatures

These need the kernel’s drivetemp module, which ships with the kernel but is usually not loaded. To load it now and at every boot:

sudo modprobe drivetemp
echo drivetemp | sudo tee /etc/modules-load.d/drivetemp.conf

Restart tuxctl afterwards: it looks for sensors at startup. To undo, delete /etc/modules-load.d/drivetemp.conf.

Hard disks: per the kernel documentation, reading the temperature may reset the spin-down timer on some drives (observed with WD120EFAX). tuxctl reads it every 2 seconds, so such a drive would never spin down. SSDs do not spin, so this does not concern them. If you rely on hard disks spinning down, leave drivetemp unloaded.

CPU power

Intel and AMD CPUs report their package energy through RAPL (/sys/class/powercap/intel-rapl:N/energy_uj), but since Linux 5.10 only root can read it: unprivileged access allowed a side-channel attack on the CPU (PLATYPUS, CVE-2020-8694). tuxctl shows the package power when the counter is readable, computed from the energy used between two readings 2 seconds apart, and nothing otherwise. The out-of-tree zenpower driver (AMD Zen 1–3) reports power directly and needs no step.

To make only the energy counters readable, now and at every boot:

echo 'ACTION=="add", SUBSYSTEM=="powercap", KERNEL=="intel-rapl:[0-9]*", RUN+="/usr/bin/chmod a+r /sys%p/energy_uj"' | sudo tee /etc/udev/rules.d/90-rapl-energy.rules
sudo udevadm trigger --subsystem-match=powercap --action=add

Restart tuxctl afterwards. This lets every local user read the energy counters again, and so reopens that side channel; weigh it on a shared machine. To undo, delete the rule and reboot.

Some monitors are instead installed with the cap_dac_read_search capability, which lets them read every file on the system regardless of its permissions; tuxctl does not need or recommend that.

NVIDIA GPUs

GPUs on NVIDIA’s proprietary driver report through NVML, which tuxctl loads at run time. The static release binaries cannot load it; use a glibc build instead. On x86_64 that can be the release’s tuxctl-x86_64-unknown-linux-gnu.tar.gz (glibc 2.28 or newer; see Installation); anywhere, a build from source:

cargo install tuxctl --locked

NVML adds about 20 MiB of memory and some CPU time; see NVIDIA GPUs on the proprietary driver for the figures, and --no-nvidia-temperature to leave it unloaded.

Design and reliability

tuxctl is built around a small, bounded, event-driven flow:

/proc, /sys, systemctl, journalctl
        │   background collectors
        ▼
bounded snapshots (latest value only)
        │
        ▼
App state ◀── keyboard and mouse actions
        │
        ▼
render from cached state ──▶ hit regions for the mouse

Staying cheap

  • Linux data is collected on background threads; drawing uses only cached state and never reads /proc or /sys or waits on systemctl or journalctl.
  • Snapshots that arrive together cost one redraw, and background redraws are limited to one every 50 ms; keyboard, mouse and resize still redraw at once.
  • Mouse movement redraws only when the element under the pointer changes.
  • Screens that are not visible update their data without redrawing.
  • systemctl and journalctl start only when their screens are first opened; services are collected only while their screen is visible, and hardware sensors only while the Overview is.
  • Histories, the log buffer and every queue between threads have a fixed size.

Staying correct

  • If a collector stops delivering data, the frame title shows a stale marker for that screen instead of presenting frozen data as live.
  • A systemctl call is stopped after 10 seconds, so it cannot stall the Services screen or exit.
  • An active search or filter stays visible in the status line; messages appear after it, never instead of it.
  • SIGTERM, SIGHUP and SIGINT quit through the same path as q, so the terminal is always restored. A crash in a background thread does not touch the terminal.

Security

  • Untrusted text: process names, command lines, journal messages, unit descriptions and other system data can be set by other users. Control characters and bidirectional overrides are removed before anything reaches the terminal, so escape sequences planted in them cannot act on it.
  • Bounded input: command lines are read up to 4 KiB per process, and journal fields over 4 KiB arrive empty from journalctl, so other users cannot make tuxctl hold large amounts of memory.
  • Signals: a signal goes through a pidfd to the process whose (PID, start time) was confirmed; a process that exited or a new one that reused the PID is never signaled, and without pidfds nothing is sent.
  • No privileges: tuxctl asks for none and changes nothing on the system; features that need root-only data are simply not shown.
  • Code: every unsafe block states why it is sound, and code outside tests may not panic on errors (both enforced by clippy). Dependencies are checked against RustSec advisories on every push and weekly.

To report a vulnerability, see the security policy.

Testing

Every push and pull request runs the unit and command-line tests, clippy, the minimum supported Rust version, a pseudo-terminal smoke test of the release binary, and guards on redraw rates and memory. See Development.

Performance

Reference measurements for tuxctl, how they are made, and how releases compare. These numbers come from one machine; they are a baseline for regressions, not a guarantee for other systems. The limits that CI enforces on every push are listed in scripts/README.md.

Reference machine

  • AMD Ryzen 5 7500F (6 cores, 12 threads), CachyOS, Linux 7.2.6
  • CPU governor and platform profile: performance; transparent huge pages: always
  • Rust 1.98.1
  • tuxctl runs in a 160×50 pseudo-terminal driven by scripts/ptyrun.py, at the default 1 s interval. The cost of a real terminal emulator is not included.

Method

cargo build --release --locked --features redraw-counter --target-dir target/counter
scripts/measure.py target/counter/release/tuxctl --json run1.json   # three runs, report the median
cargo build --release --locked
scripts/rss.py target/release/tuxctl 15 --json rss.json

Each measure.py scenario lasts 20 s. CPU is the percentage of one core (user + system time from /proc/<pid>/stat); RSS is VmRSS. The storm writes 200 journal messages per second.

Comparing two versions: run measure.py three times on each, on the same machine and power profile. The noise band of a metric is the spread (max − min) of the older version’s runs. A newer median is unchanged if it is within the larger of twice that band and a floor of 0.15 CPU percentage points, 0.3 redraws/s, or 256 KiB. After minute 5, RSS should grow by at most 128 KiB over a 15-minute run.

v0.3.4

Release binary: x86_64-unknown-linux-musl, statically linked, 1.77 MB (v0.3.3: 1.74 MB). Median of three runs, alternated with three runs of the v0.3.3 musl binary in the same session (in brackets), on Linux 7.2.8.

ScenarioCPU %Redraws/sRSS
Overview, idle0.70 (0.70)2.00 (2.00)2.7 MiB
Processes, idle0.70 (0.70)1.10 (1.25)2.8 MiB
Logs, idle0.65 (0.60)0.05 (0.05)2.9 MiB
Logs, 200 journal messages/s0.93 (1.00)3.87 (3.93)3.2 MiB
Mouse hover at 240 Hz1.50 (1.50)13.46 (13.53)3.1 MiB
  • CPU and redraws are unchanged: every difference is within the 0.15-point floor and the redraw noise band.
  • Startup RSS: 2.6 MiB (2.6 MiB). After 15 minutes on Overview: 2.7 MiB, 56 KiB more than at minute 5.
  • Time to the first frame: 2.1 ms (2.1 ms). Input latency during the log storm: 0.70 ms (0.73 ms).
  • The new glibc binary loads NVML for the NVIDIA GPU (an RTX 5070 Ti that cannot runtime-suspend): Overview idle 1.05 % and 31 MiB. NVML is now closed once the Overview has been hidden for 10 s, so the other screens run at 11 MiB instead of about 31 MiB (Processes idle 0.70 %, 11.0 MiB); returning to the Overview loads it again in about 40 ms on the worker.

v0.3.3

Release binary: x86_64-unknown-linux-musl, statically linked, 1.74 MB (v0.3.0: 1.45 MB). Median of three runs, alternated with three runs of the v0.3.0 musl binary in the same session (in brackets). Static binaries do not load NVML, so these figures leave it out; see below for a glibc build.

ScenarioCPU %Redraws/sRSS
Overview, idle0.60 (0.55)2.00 (1.10)2.7 MiB
Processes, idle0.60 (0.65)1.35 (2.00)2.8 MiB
Logs, idle0.55 (0.50)0.00 (0.15)2.9 MiB
Logs, 200 journal messages/s0.80 (0.80)3.87 (3.87)3.2 MiB
Mouse hover at 240 Hz1.39 (1.39)13.43 (13.52)3.2 MiB
  • CPU is unchanged: every difference is within the 0.15-point floor.
  • Idle redraws moved between tabs rather than grew: Overview +0.9/s, Processes −0.65/s. Bisected to the temperature work, whose longer metrics sample changes which collector updates land in the same frame (the main loop renders once for updates that arrive together). The redraw guards still pass.
  • Startup RSS: 2.7 MiB (v0.3.0: 2.4 MiB); the difference is about the binary’s growth. After 15 minutes on Overview: 2.7 MiB, 8 KiB more than at minute 5.
  • Time to the first frame: 1.8 ms. Input latency during the log storm: 0.7 ms (0.7 ms).
  • A glibc build on the same machine loads NVML for its NVIDIA GPU: startup RSS is about 31 MiB, Overview idle about 0.95 % and Processes idle about 0.60 % (hardware sensors are read only while the Overview is visible).
  • Another tuxctl was running on the machine during these runs; both versions ran alongside it.

v0.3.0

Release binary: x86_64-unknown-linux-musl, statically linked, 1.45 MB. Median of three runs, alternated with three runs of the v0.2.7 musl binary in the same session (in brackets).

ScenarioCPU %Redraws/sRSS
Overview, idle0.60 (0.60)1.10 (1.00)2.3 MiB
Processes, idle0.75 (0.70)2.00 (2.00)2.4 MiB
Logs, idle0.60 (0.55)0.00 (0.00)2.5 MiB
Logs, 200 journal messages/s0.87 (0.87)3.87 (3.87)2.8 MiB
Mouse hover at 240 Hz1.39 (1.29)13.54 (13.43)2.8 MiB
  • Every difference is within the noise band: the v0.2.7 hover runs alone spread by 0.20 points.
  • Startup RSS: 2.3 MiB (v0.2.7: 2.2–2.3 MiB). After 15 minutes on Overview: 2.3 MiB, unchanged since minute 5.
  • Time to the first frame: 2 ms. Input latency during the log storm: 0.9 ms (0.75 ms).
  • The v0.2.7 figures below came from an earlier session and read about 0.05–0.1 points lower on some scenarios than v0.2.7 measured again here; compare versions only within one session.

v0.2.7

Release binary: x86_64-unknown-linux-musl, statically linked, 1.39 MB.

ScenarioCPU %Redraws/sRSS
Overview, idle0.601.202.3 MiB
Processes, idle0.652.002.4 MiB
Logs, idle0.550.002.5 MiB
Logs, 200 journal messages/s0.803.872.8 MiB
Mouse hover at 240 Hz1.2913.412.7 MiB
  • Startup RSS: 2.2 MiB. After 15 minutes on Overview: 2.3 MiB, unchanged since minute 5.
  • Time to the first frame: 2 ms. Input latency during the log storm: 0.7 ms.
  • RSS change over two further 15 s storms with a full log buffer: within ±60 KiB.

A cargo install build (glibc, dynamically linked, 1.27 MB) measured 0.55 / 0.55 / 0.50 / 0.67 / 1.10 % CPU for the same scenarios, with the same redraw rates. Its RSS is about 4.8 MiB at startup and 5.2 MiB after 15 minutes (+4 KiB since minute 5). A dynamically linked process also counts the pages of the shared C library it maps, so RSS is higher than for the static build.

History

Same machine, glibc builds, measure.py as of v0.2.7, median of three runs.

Scenariov0.2.5 CPU %v0.2.7 CPU %v0.3.0 CPU %v0.2.5 redraws/sv0.2.7 redraws/sv0.3.0 redraws/s
Overview, idle0.550.550.551.401.251.10
Processes, idle0.600.550.602.002.002.00
Logs, idle0.550.500.550.000.050.10
Logs, 200 journal messages/s0.670.670.673.873.873.93
Mouse hover at 240 Hz1.201.101.1913.5613.5413.42
v0.2.5v0.2.7v0.3.0
Startup RSS5.1 MiB4.8 MiB4.9 MiB
RSS after 15 minutes5.4 MiB¹5.2 MiB5.1 MiB²
Binary size1.94 MB1.27 MB1.33 MB

¹ Measured before v0.2.7 with the same rss.py run: 5 496 kB, flat after minute 4. ² 5 264 kB, +4 KiB since minute 5; v0.2.7 measured 5 200 kB (+0 KiB) in the same session.

v0.3.0 was measured interleaved with three fresh v0.2.7 runs on the same day (v0.2.7: 0.55 / 0.60 / 0.50 / 0.67 / 1.09 % CPU, 1.10 / 2.00 / 0.00 / 3.93 / 13.48 redraws/s); every difference is within the noise band. Time to the first frame stayed at about 1.9 ms and storm input latency at about 0.5 ms. Single runs of each intermediate v0.3.0 commit stayed within the same band, so no item stands out. The sanitization pass over every frame does not show in the hover scenario, the most render-heavy one. The binary grew by 4.8 % (disk I/O, pinning, filters, menu, sparklines). The Logs idle redraws come from host journal traffic, as noted below.

Shortest interval (--interval 250ms)

One run each, glibc builds; the idle guard is 9.5 redraws/s at this interval.

Scenariov0.2.7 CPU %v0.3.0 CPU %v0.2.7 redraws/sv0.3.0 redraws/s
Overview, idle0.750.804.004.00
Processes, idle0.800.853.803.85
Logs, idle0.650.700.150.00
Logs, 200 journal messages/s0.800.803.873.87
Mouse hover at 240 Hz1.291.3913.4813.58

Four times as many metrics and network samples cost about 0.2 CPU percentage points on the idle screens and four redraws per second on Overview: the metrics and network updates usually arrive together and share one redraw.

v0.2.7 changes no collector cadence or rendering path; the CPU and redraw differences above are within the noise band. The smaller binary and the roughly 200 KiB lower RSS come from the release profile (LTO, one codegen unit, stripped symbols).

Overview idle redraws vary between runs (1.0 to 1.4 per second): the metrics, process, and network collectors each publish once per second, and their updates share a redraw only when they arrive close together.

Sources of noise

  • Tick resolution: CPU time is counted in 10 ms clock ticks, so a 20 s idle scenario at 0.5 % is only about 10 ticks.
  • Background load and power profile: keep them the same across compared runs.
  • Journal traffic: Logs idle redraws depend on how much the host writes to the journal.
  • Transparent huge pages: with THP set to always, a run can show about 2 MiB more RSS. measure.py and rss.py record AnonHugePages to identify such runs.
  • Warm-up: the command cache and hardware discovery fill during the first minutes, so judge memory growth from minute 5 on.
  • CI runners vary by CPU model and neighbours: idle CPU on GitHub-hosted runners ranged from 0.25 % to 0.62 % for the same scenario. CI therefore enforces only redraw limits, liveness, and catastrophe limits, and reports CPU and RSS.

Terminal support and limitations

Terminal size

The normal interface needs at least 40 columns × 15 rows. Below that, tuxctl shows a terminal-too-small message instead.

Above it, layouts adapt: on narrow terminals the tab bar uses short labels (Ovr Proc Svc Logs Net), and Overview cards that do not fit are left out in priority order rather than drawn empty (see Overview). Long values may be cut short in narrow layouts; there is no horizontal scrolling.

Colors

tuxctl picks true color, 256 colors or the 16 basic colors from COLORTERM and TERM at startup (see Overview).

Known limitations

  • Linux only: tuxctl relies on Linux /proc, /sys, systemd tools and Linux-specific process signaling.
  • systemd: the Services and Logs screens need systemctl and journalctl.
  • NVIDIA GPUs: on the proprietary driver they need a glibc build (the x86_64 glibc release binary, or a source build); the static release binaries cannot load NVML. There is no aarch64 glibc release binary.
  • Hardware hotplug: hardware is discovered at startup; newly attached devices appear after a restart.
  • Permissions: signaling other users’ or privileged processes follows the normal Linux permission rules.
  • pidfds: signaling needs pidfd support; tuxctl does not fall back to PID-only signaling.

Development

git clone https://github.com/Seqat/tuxctl.git
cd tuxctl
cargo run                    # debug build
cargo build --release        # optimized build in target/release/tuxctl

Work happens on the dev branch; main receives releases. The contributing guide has the branch and commit conventions, the code guidelines, and where tests go.

Checks

These run on every push and pull request, and should pass before a change is proposed:

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test

The minimum supported Rust version is 1.88 (cargo +1.88 check --all-targets).

Scripts

scripts/ has the tools used for releases and performance work:

  • smoke.py drives the release binary in a pseudo-terminal and checks keys, resizing, exit and terminal restoration.
  • measure.py measures CPU, memory and redraw rates in standard scenarios; with --check it enforces the guards CI uses.
  • rss.py watches memory over a long run.

See scripts/README.md for their options, and Performance for how measurements are made and compared.

This documentation

The pages live in docs/ and are built with mdBook:

mdbook serve docs --open

The key tables on Controls are checked by a test against the real key bindings, so they cannot drift from the code.