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,systemctlandjournalctldirectly and asks for no privileges.
Start with Installation, then the Controls.
Installation
Requirements
- Linux.
tuxctluses Linux interfaces such as/procand/sysdirectly. systemctlfor the Services screen andjournalctlfor 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]
| Option | Description |
|---|---|
--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-temperature | Do 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. |
--check | Print which sensors tuxctl finds on this machine, their values and origins, and what would enable the missing ones; then exit. See Optional setup. |
-h, --help | Print help. |
-V, --version | Print 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
| Key | Action |
|---|---|
1 - 5 | Switch directly to tab: Overview, Processes, Services, Logs, Network |
Tab / Shift+Tab | Next / previous tab |
→ / ← | Next / previous tab |
? | Toggle Help dialog |
+ / - | Longer / shorter sampling interval (250ms to 60s, shown as ⟳ in the top-right corner) |
Esc | Dismiss dialog / clear the search, then the view filter / open the main menu |
q | Open the main menu on Exit; Enter or q again quits |
Ctrl+C | Quit immediately, from anywhere |
Navigation & Common Actions
| Key | Action |
|---|---|
↑ / k | Move selection up |
↓ / j | Move selection down |
PageUp / PageDown | Move selection by page |
Home / End | Jump to first / last item |
/ | Begin search / filter; ↑ / ↓ and PageUp / PageDown move through the matches while typing |
Enter | Open detailed inspection |
Processes
| Key | Action |
|---|---|
c | Sort by CPU % |
m | Sort by Memory |
p | Sort by PID |
n | Sort by Name |
T / Shift+T | Request SIGTERM for selected process |
K / Shift+K | Request SIGKILL for selected process |
P / Shift+P | Pin / unpin the selected process (up to 8) |
Shift+↑ / Shift+↓ | Move the selected pinned process up / down (Alt+↑ / Alt+↓ also work) |
v | Hide / 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
| Key | Action |
|---|---|
Tab / ← / → / h / l | Move focus between Cancel and confirmation |
Enter | Execute focused action |
Esc | Cancel and close |
Main Menu
Esc opens the main menu when there is no dialog, search, or view filter to clear.
| Key | Action |
|---|---|
↑ / ↓ | Move between About and Exit (k / j also work) |
Enter | Open About, or exit tuxctl |
q | Exit tuxctl |
Esc | Close the menu (from About, go back to the menu) |
Services
| Key | Action |
|---|---|
r | Request an immediate service refresh |
v | Cycle the view: all units → loaded units (hide not-found) → failed units |
Logs
| Key | Action |
|---|---|
f | Toggle follow mode |
Space | Pause / resume |
v | Cycle 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, orMEMORYheaders. - 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
Cancelor the confirmation action. - Main menu: Click
AboutorExit. - 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
Pon the Processes screen, with their CPU and memory. A pinned process that exits stays for a few seconds, dimmed, asexited.
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:
| Value | Color |
|---|---|
| below 10 % | light blue |
| 10 % to 65 % | green |
| 65 % to 80 % | yellow |
| 80 % to 95 % | orange |
| 95 % and above | red |
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,porn, or by clicking a column header; sorting again reverses the order. - Search:
/filters by name, PID or command line, case-insensitively. - Kernel threads:
vhides or shows them. - Details:
Enteropens 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:
vcycles through all units, loaded units (hidingnot-found), and failed units only. - Refresh:
rasks for a new listing at once. - Details:
Entershows 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:
ffollows new entries;Spacepauses and resumes. - Search:
/filters the entries. - Priority:
vcycles the minimum priority: all, notice, warning, error. - Details:
Entershows 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:
Entershows 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.
| Component | Source |
|---|---|
| Intel CPU | coretemp: the Package id N sensor, or the hottest core when there is none |
| AMD CPU | k10temp 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 GPU | amdgpu (the edge sensor) or radeon hwmon |
| Intel GPU | i915 / xe hwmon, when the GPU has its own sensor; integrated GPUs usually do not |
NVIDIA GPU, nouveau | nouveau hwmon |
| NVIDIA GPU, proprietary driver | NVML (libnvidia-ml.so.1, installed with the driver); not in the static release binaries |
| NVMe | nvme hwmon (Composite) |
| SATA / SAS | drivetemp hwmon, only when that module is loaded (see Optional setup) |
| Network adapter | A 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:
| Component | Assumed critical temperature |
|---|---|
| CPU | 95 °C |
| GPU | 95 °C |
| NVMe | 80 °C |
| SATA / SAS and other disks | 60 °C |
| Network adapter | 100 °C |
The value is always shown, so the color never carries meaning alone.
GPU usage
| Driver | Utilization | VRAM | Power | Fan |
|---|---|---|---|---|
| NVIDIA proprietary | NVML | NVML | NVML | NVML |
amdgpu | gpu_busy_percent | mem_info_vram_* | hwmon | hwmon |
nouveau | – | – | hwmon | hwmon |
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).nvmlShutdowndoes not free it, not even with RTD3, where NVML is shut down after every reading; closing the library does.tuxctlcloses 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-temperatureleaves 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.gzrelease binary, orcargo 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 see | You need |
|---|---|
| SATA / SAS disk temperatures | the drivetemp kernel module (below) |
| CPU package power | readable RAPL energy counters (below) |
| Temperature and usage of NVIDIA GPUs on the proprietary driver | the 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).
tuxctlreads 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, leavedrivetempunloaded.
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
/procor/sysor waits onsystemctlorjournalctl. - 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.
systemctlandjournalctlstart 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
stalemarker for that screen instead of presenting frozen data as live. - A
systemctlcall 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,SIGHUPandSIGINTquit through the same path asq, 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 maketuxctlhold 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:
tuxctlasks for none and changes nothing on the system; features that need root-only data are simply not shown. - Code: every
unsafeblock states why it is sound, and code outside tests may not panic on errors (both enforced byclippy). 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
tuxctlruns in a 160×50 pseudo-terminal driven byscripts/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.
| Scenario | CPU % | Redraws/s | RSS |
|---|---|---|---|
| Overview, idle | 0.70 (0.70) | 2.00 (2.00) | 2.7 MiB |
| Processes, idle | 0.70 (0.70) | 1.10 (1.25) | 2.8 MiB |
| Logs, idle | 0.65 (0.60) | 0.05 (0.05) | 2.9 MiB |
| Logs, 200 journal messages/s | 0.93 (1.00) | 3.87 (3.93) | 3.2 MiB |
| Mouse hover at 240 Hz | 1.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.
| Scenario | CPU % | Redraws/s | RSS |
|---|---|---|---|
| Overview, idle | 0.60 (0.55) | 2.00 (1.10) | 2.7 MiB |
| Processes, idle | 0.60 (0.65) | 1.35 (2.00) | 2.8 MiB |
| Logs, idle | 0.55 (0.50) | 0.00 (0.15) | 2.9 MiB |
| Logs, 200 journal messages/s | 0.80 (0.80) | 3.87 (3.87) | 3.2 MiB |
| Mouse hover at 240 Hz | 1.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
tuxctlwas 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).
| Scenario | CPU % | Redraws/s | RSS |
|---|---|---|---|
| Overview, idle | 0.60 (0.60) | 1.10 (1.00) | 2.3 MiB |
| Processes, idle | 0.75 (0.70) | 2.00 (2.00) | 2.4 MiB |
| Logs, idle | 0.60 (0.55) | 0.00 (0.00) | 2.5 MiB |
| Logs, 200 journal messages/s | 0.87 (0.87) | 3.87 (3.87) | 2.8 MiB |
| Mouse hover at 240 Hz | 1.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.
| Scenario | CPU % | Redraws/s | RSS |
|---|---|---|---|
| Overview, idle | 0.60 | 1.20 | 2.3 MiB |
| Processes, idle | 0.65 | 2.00 | 2.4 MiB |
| Logs, idle | 0.55 | 0.00 | 2.5 MiB |
| Logs, 200 journal messages/s | 0.80 | 3.87 | 2.8 MiB |
| Mouse hover at 240 Hz | 1.29 | 13.41 | 2.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.
| Scenario | v0.2.5 CPU % | v0.2.7 CPU % | v0.3.0 CPU % | v0.2.5 redraws/s | v0.2.7 redraws/s | v0.3.0 redraws/s |
|---|---|---|---|---|---|---|
| Overview, idle | 0.55 | 0.55 | 0.55 | 1.40 | 1.25 | 1.10 |
| Processes, idle | 0.60 | 0.55 | 0.60 | 2.00 | 2.00 | 2.00 |
| Logs, idle | 0.55 | 0.50 | 0.55 | 0.00 | 0.05 | 0.10 |
| Logs, 200 journal messages/s | 0.67 | 0.67 | 0.67 | 3.87 | 3.87 | 3.93 |
| Mouse hover at 240 Hz | 1.20 | 1.10 | 1.19 | 13.56 | 13.54 | 13.42 |
| v0.2.5 | v0.2.7 | v0.3.0 | |
|---|---|---|---|
| Startup RSS | 5.1 MiB | 4.8 MiB | 4.9 MiB |
| RSS after 15 minutes | 5.4 MiB¹ | 5.2 MiB | 5.1 MiB² |
| Binary size | 1.94 MB | 1.27 MB | 1.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.
| Scenario | v0.2.7 CPU % | v0.3.0 CPU % | v0.2.7 redraws/s | v0.3.0 redraws/s |
|---|---|---|---|---|
| Overview, idle | 0.75 | 0.80 | 4.00 | 4.00 |
| Processes, idle | 0.80 | 0.85 | 3.80 | 3.85 |
| Logs, idle | 0.65 | 0.70 | 0.15 | 0.00 |
| Logs, 200 journal messages/s | 0.80 | 0.80 | 3.87 | 3.87 |
| Mouse hover at 240 Hz | 1.29 | 1.39 | 13.48 | 13.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.pyandrss.pyrecordAnonHugePagesto 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:
tuxctlrelies on Linux/proc,/sys, systemd tools and Linux-specific process signaling. - systemd: the Services and Logs screens need
systemctlandjournalctl. - 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;
tuxctldoes 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.pydrives the release binary in a pseudo-terminal and checks keys, resizing, exit and terminal restoration.measure.pymeasures CPU, memory and redraw rates in standard scenarios; with--checkit enforces the guards CI uses.rss.pywatches 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.