Technical Manual

Version1.0.0
DateOctober 2026
Chapter 1

Overview

1.1 · What ngtop is

ngtop is a system monitor for the terminal. It joins the processor, memory and process view of btop, the GPU view of nvtop and the network, disk and service views of a graphical task manager in one full-screen interface with five sections. It is one static executable for Linux x86_64: copy it to any distribution and it runs, with nothing to install. This manual explains how it works, for whoever wants to understand it, maintain it or extend it.

One process, one static executableCollectorsread /proc, /sys, /etc, D-BusSystem + Applatest data + historyInterfaceratatui widgets, sectionsLinux kernelprocfs, sysfs, DRM, cgroupssystemdD-Bus: units, sessions, polkitTerminalkeys, mouse, coloursThe collectors know nothing about the interface; the interface only reads what they have collected.Figure 1.1 — ngtop in one picture: collect, keep, draw
ItemValue
Version1.0.0 (Cargo.toml)
LanguageRust, edition 2024; interface drawn with ratatui 0.30 on crossterm 0.29
Targetx86_64-unknown-linux-musl, fully static (ch. 9 “The static build”)
SectionsOverview, GPU, Processes, Network & Disks, Services (keys 1 to 5)
DataOnly the kernel's text files, a few system calls through rustix, and D-Bus for systemd (ch. 4 “Data sources”)
PrivilegesNone required. Whatever needs more (other users' file descriptors, power, GPU clients of other users) is shown as “—” when it cannot be read
GPUsAMD (amdgpu) and Intel (i915, xe) are read; NVIDIA is only detected
LicencePolyForm Noncommercial 1.0.0 (LICENSE.md): free for noncommercial use
Size19,322 lines of Rust, unit tests included; 177 test functions
Table 1.1 — ngtop at a glance

1.2 · Design principles

The design is the same in every file. These rules were fixed before the code was written and they explain choices that would otherwise look odd, such as a services thread that never shares a lock with the interface, or a signal dialog that checks a process twice.

PrincipleWhat it means in the codeWhere to see it
Collect and draw are separateA collector reads files and computes numbers; it has no idea that a screen exists. A view reads the numbers; it never opens a file. The two halves can be tested apartsrc/collect/ and src/ui/; ch. 3 “Architecture”
One root for every pathEvery collector receives the roots of /proc, /sys and /etc as a Roots value. Tests and screenshots point it at a fake treeRoots in src/collect/mod.rs
No panic on system dataA missing file, a permission error or a strange value gives None or “—”, never a crash. Arithmetic on counters is saturating; every number that is drawn is clampedch. 12 “Conventions”
Pure Rust dependenciesNo crate that compiles C code, so the result is one static filech. 9 “The static build”
Never wake a sleeping GPUThe metrics of a discrete card in a low-power state are not read; its clients are still counted from the files of the processesch. 7 “Safety”
Never act on the wrong processA signal goes to a process identified by its pid and its start time, through a pidfd when the kernel has one7.1 “Signals: pidfd and start time”
The interface never waitsEverything slow (D-Bus, subprocesses, statvfs on a hung network mount) runs in another thread; the interface asks and later collects the answer3.4 “Threads”
Cheap by designStatic data is read once, slow-changing data every few cycles, expensive data only when the section that needs it is visiblech. 8 “Performance”
Say what is missingA section without its data says so on screen (no GPU detected, systemd not found) instead of showing zerosch. 6 “The sections”
Table 1.2 — The principles that shaped ngtop
i
Language. Texts shown on screen are in English. Comments in the source are short and are still in Italian (ch. 12 “Conventions”).

1.3 · The project in numbers

The numbers below are counted from the sources every time this manual is built, so they cannot drift. Unit tests live next to the code they test, inside the same files, and are counted with them.

PartWhereLinesContents
Collectorssrc/collect/6,505Readers of /proc, /sys, D-Bus; the services thread; unit tests
Interfacesrc/ui/10,425Application state, widgets, the five sections, colours, time zone; unit tests
Entry point and settingssrc/main.rs, src/lib.rs, src/config.rs1,082Command line, diagnostics modes, the configuration file
Integration teststests/1,310Collectors and views on fake system trees
Build and toolsbuild.sh, scripts/, site/1,347Static build, distribution test, third-party licence list, the web site
This manualdocs/sources/4,948The generator, the style and one file per chapter
Total25,617
Table 1.3 — The project in numbers

Besides code, tests/fixtures/ holds the fake system trees (snapshots of /proc, /sys and /etc reduced to the files ngtop reads), and mockup/ holds the HTML mock-ups the interface was designed from; mockup/app.html is the visual reference.

Chapter 2

Source tree and tools

2.1 · The repository

Everything is in one Cargo package. The library holds the collectors and the interface; the program is a thin main.rs on top of it, so the integration tests can call the same functions the program does.

NG-TOP/
├── Cargo.toml  # one package, release profile tuned for size and speed
├── build.sh  # static build + check
├── src/
│   ├── main.rs  # entry point, diagnostic modes
│   ├── lib.rs  # library root
│   ├── config.rs  # command line and configuration file
│   ├── collect/  # collectors: no interface code
│   │   ├── mod.rs  # System, Roots
│   │   ├── cpu.rs  mem.rs  load.rs  sensors.rs
│   │   ├── process.rs  procdetail.rs
│   │   ├── net.rs  disk.rs  conn.rs  gpu.rs
│   │   └── services/  # systemd: backend, worker thread, cgroup, fake
│   └── ui/  # interface: ratatui
│       ├── mod.rs  app.rs  view.rs  keys.rs  doc.rs  history.rs  fmt.rs  theme.rs  tz.rs  snapshot.rs
│       ├── widgets/  # Panel, BrailleGraph, Meter, Sparkline, ProcTable, TabBar, KeyBar, Dialog
│       └── sections/  # one file per section; servizi/ is a folder
├── tests/  # integration tests
│   └── fixtures/  # fake /proc, /sys, /etc trees
├── scripts/  # test-distro.sh, third-party.py
├── site/  # landing page, publishing, licence page
├── mockup/  # HTML mock-ups of the interface (visual reference)
├── docs/  # these manuals
│   └── sources/  # generator, style, chapters
└── dist/  # the finished binary (not in the repository)
Table 2.1 — The repository, folder by folder
i
Names. Section files and some module names keep their original Italian names (panoramica.rs, processi.rs, retedischi.rs, servizi/). The screen names are English: Overview, Processes, Network & Disks, Services. The correspondence is in ch. 13 “Glossary”.

2.2 · Tools and build commands

Four tools are enough to build and test ngtop. Rust is installed with rustup in ~/.cargo/bin; add it to PATH if it is missing.

CommandWhat it does
cargo buildDebug build for the machine's own libc, for quick tries
cargo testAll unit and integration tests
cargo clippy --all-targets -- -D warningsLint; warnings are errors
cargo fmtFormatting
build.shRelease build for x86_64-unknown-linux-musl, copy to dist/ngtop, check that it is static
scripts/test-distro.shRuns dist/ngtop in containers of several distributions (ch. 10 “Testing”)
scripts/third-party.pyRegenerates the list of third-party licences
python3 docs/sources/build.pyRegenerates these manuals (--check verifies they match the code)
Table 2.2 — The commands of the project

What build.sh does. It stops at the first error (set -euo pipefail), puts ~/.cargo/bin on the path, runs cargo build --release --target x86_64-unknown-linux-musl, copies the result to dist/ngtop (after removing the old file, so a running copy does not give “text file busy”), and finally runs ldd on it. If ldd does not say “not a dynamic executable” or “statically linked”, the script prints the dependencies and exits with an error. Only then is the file called static: see ch. 9 “The static build”.

The release profile (Cargo.toml): opt-level = 3, lto = "fat" (the whole program is optimised together), codegen-units = 1 and strip = true (no symbols). It is slower to compile and makes the file smaller and faster.

The last line of a successful build
$ bash build.sh
...
OK: dist/ngtop is static (3.5M)

2.3 · Folders outside src

Each folder outside src/ has one job.

FolderJob
scripts/test-distro.sh starts the binary in containers (podman, rootless) of eight distributions and checks exit codes and output; third-party.py reads cargo tree and cargo metadata and writes the licence list of the crates in the binary
site/The web site of the project: the landing page with a demonstration terminal, the licence page built from LICENSE.md, and publish.sh that builds site/public/ and uploads it. The manuals of docs/ are published with it
mockup/HTML pages from which the interface was designed. mockup/tui.js holds the original gradients; src/ui/theme.rs repeats them in Rust
tests/fixtures/Fake system trees, described in ch. 10 “Testing”
docs/The two manuals, decisions-and-history.md (why ngtop is the way it is) and the generator in docs/sources/
dist/Output of build.sh. Not part of the sources
target/Cargo's build folder. Not part of the sources
Table 2.3 — The folders outside src

2.4 · The file map

Every source file with its line count. The counts are taken from the files at every build of the manual. A file that exists but is not described here would appear under “Other files” at the end, with a dash instead of a role: that is the sign that this list needs a new line.

FileLinesRole
Entry point and settings
src/main.rs407Starts ngtop: reads the command line, runs the interface, or one of the diagnostic modes (--dump, --snapshot, --bench, --bench-start, --doc-json)
src/lib.rs8Library root: declares collect, config and ui, so the tests can use the program as a crate
src/config.rs667Command line, the configuration file (a small TOML subset), merging of the two, atomic saving with W
Collectors: src/collect/
src/collect/mod.rs303System, the state of the whole machine, and Roots; refresh() runs every collector in order
src/collect/cpu.rs197CPU times from /proc/stat, model and topology, frequencies
src/collect/mem.rs100Memory and swap from /proc/meminfo
src/collect/load.rs78Load average and uptime
src/collect/sensors.rs96CPU temperature: which hwmon or thermal zone to read
src/collect/process.rs604Every process from /proc/[pid]: CPU%, caches, tree order, sorting
src/collect/procdetail.rs709The extra detail of the one selected process (threads, files, sockets, environment, limits, cgroup)
src/collect/net.rs360Interfaces, rates, routes, addresses, DNS
src/collect/disk.rs902Disks (IOPS, latency, queue), mounts, and the statvfs worker threads
src/collect/conn.rs572TCP/UDP connections and the socket-inode to pid cache
src/collect/gpu.rs954GPU discovery, amdgpu / i915 / xe metrics, per-process use from fdinfo
src/collect/services/mod.rs407systemd data model, the Backend trait, boot phases, parsers of journal and blame output
src/collect/services/dbus.rs604The real backend: D-Bus through zbus, subprocesses with a time limit
src/collect/services/worker.rs277The services thread and its request / response channels
src/collect/services/cgroup.rs132Memory and CPU% of a unit, read straight from its cgroup
src/collect/services/fake.rs210A fake backend with demo data, for tests and screenshots
Interface: src/ui/
src/ui/mod.rs171Terminal set-up and restore, signal flags, the event loop
src/ui/app.rs1,060App: all interface state, key and mouse handling, histories
src/ui/view.rs127Draws one frame: tab bar, the section, key bar, status line, help and dialogs
src/ui/keys.rs177The key tables: bottom bars and the F1 help. The single source, also used by --doc-json
src/ui/doc.rs105The hidden --doc-json output: keys and command-line options as JSON, for the manuals
src/ui/history.rs104Fixed-size ring buffer of samples
src/ui/fmt.rs126Number formatting: bytes, rates, uptime, temperature
src/ui/theme.rs265Colours, gradients, and the conversion to 256 and 16 colours
src/ui/tz.rs581Local time zone in pure Rust (TZ variable, TZif files)
src/ui/snapshot.rs196Renders one frame on a test backend, as text or ANSI colours
src/ui/widgets/mod.rs69Safe writing into the buffer (put, putn, clip)
src/ui/widgets/braille.rs126The braille graph
src/ui/widgets/meter.rs82The bar of filled cells
src/ui/widgets/sparkline.rs69The one-line sparkline
src/ui/widgets/panel.rs116The panel with a rounded border and titles on the border
src/ui/widgets/proctable.rs254Sortable table with fixed and flexible columns
src/ui/widgets/tabbar.rs128The top line: brand, tabs, host information
src/ui/widgets/keybar.rs74The bottom key bar
src/ui/widgets/dialog.rs114The centred dialog window
src/ui/sections/mod.rs10Declares the five sections
src/ui/sections/panoramica.rs933Section 1, Overview, large and compact layouts
src/ui/sections/gpu.rs731Section 2, GPU, and the GPU pieces the Overview reuses
src/ui/sections/processi.rs1,973Section 3, Processes: table, detail tabs, signals, nice, folding
src/ui/sections/retedischi.rs1,041Section 4, Network & Disks
src/ui/sections/servizi/mod.rs959Section 5, Services: state, scheduling of requests, keys
src/ui/sections/servizi/draw.rs834Drawing of the Services section
Tests
tests/cli.rs97The program without a terminal: help, version, refusal to start
tests/collect.rs163Collectors on two fake snapshots of the system
tests/gpu.rs316GPU collectors and views on the GPU fixtures
tests/retedischi.rs240Network, disks and connections: collectors, views, keys
tests/servizi.rs344The Services section with a fake backend
tests/ui.rs150Overview at many sizes, many-core machines
Build, tools and web site
Cargo.toml23Dependencies, release profile (LTO, one codegen unit, strip)
build.sh19Static build and the check that the result has no dynamic dependencies
scripts/test-distro.sh88Runs the static binary in containers of several distributions
scripts/third-party.py149Writes the list of third-party licences of the crates compiled into the binary
site/publish.sh122Builds and publishes the web site
site/licence-page.py190Builds the licence page from LICENSE.md
site/landing/index.html231The landing page
site/landing/ngterm.css70Style of the demonstration terminal on the page
site/landing/ngterm.js194The demonstration terminal
site/landing/tui.js284The interface drawing code of the demonstration
This manual
docs/sources/build.py1,306Generates the two manuals: text, table and SVG figure helpers, numbering, the file map and the numbers, the screenshots, and the --check checks
docs/sources/manual.js326The script embedded in the page: sidebar, search, figure zoom
docs/sources/style.css148The common style sheet every manual page starts with
docs/sources/technical/ch01_overview.py102Technical Manual, chapter 1: Overview
docs/sources/technical/ch02_source_tree.py207Technical Manual, chapter 2: Source tree and tools
docs/sources/technical/ch03_architecture.py166Technical Manual, chapter 3: Architecture
docs/sources/technical/ch04_data_sources.py349Technical Manual, chapter 4: Data sources
docs/sources/technical/ch05_ui_framework.py226Technical Manual, chapter 5: The UI framework
docs/sources/technical/ch06_sections.py145Technical Manual, chapter 6: The sections
docs/sources/technical/ch07_safety.py164Technical Manual, chapter 7: Safety
docs/sources/technical/ch08_performance.py65Technical Manual, chapter 8: Performance
docs/sources/technical/ch09_static_build.py76Technical Manual, chapter 9: The static build
docs/sources/technical/ch10_testing.py126Technical Manual, chapter 10: Testing
docs/sources/technical/ch11_extending.py125Technical Manual, chapter 11: Extending ngtop
docs/sources/technical/ch12_conventions.py52Technical Manual, chapter 12: Conventions
docs/sources/technical/ch13_glossary.py61Technical Manual, chapter 13: Glossary
docs/sources/user/ch01_introduction.py61User Manual, chapter 1: Introduction
docs/sources/user/ch02_getting_started.py106User Manual, chapter 2: Getting started
docs/sources/user/ch03_the_screen.py134User Manual, chapter 3: The screen
docs/sources/user/ch04_overview.py106User Manual, chapter 4: Overview
docs/sources/user/ch05_gpu.py111User Manual, chapter 5: GPU
docs/sources/user/ch06_processes.py155User Manual, chapter 6: Processes
docs/sources/user/ch07_network_disks.py117User Manual, chapter 7: Network & Disks
docs/sources/user/ch08_services.py148User Manual, chapter 8: Services
docs/sources/user/ch09_configuration.py103User Manual, chapter 9: Configuration
docs/sources/user/ch10_permissions.py67User Manual, chapter 10: Permissions and running as root
docs/sources/user/ch11_troubleshooting.py107User Manual, chapter 11: Troubleshooting
docs/sources/user/ch12_key_reference.py44User Manual, chapter 12: Key reference
docs/sources/user/ch13_glossary.py45User Manual, chapter 13: Glossary
Table 2.4 — The file map
Chapter 3

Architecture

3.1 · Collect and draw

ngtop has two halves. The collectors (src/collect/) read the kernel's files and turn counters into numbers: percentages, rates, latencies. The interface (src/ui/) keeps the recent past of those numbers and draws them. The only thing that ties them is the System structure, which the interface reads and never writes.

Files and D-Bus/proc /sys /etcCollectorssrc/collect/Systemlatest dataAppstate, historiesViewssrc/ui/sections/Terminalone buffer per frameFigure 3.1 — The data path, left to right: nothing on the right of a box reaches back into the box on its left

System (src/collect/mod.rs) holds the latest value of everything: cpu, mem, load, uptime, processes, net, gpu, disks, conns, cpu_temp and zram. It also holds the previous sample of the counters it needs for differences (for example prev_cpu, prev_cores and prev_procs). A collector never stores history: it knows only “now” and “the sample before”.

App (src/ui/app.rs) keeps what the interface needs to remember: the histories (cpu_hist, mem_hist, and one per network interface, disk and GPU), the process list in the order shown, the selection, the filter, the open dialog and the state of each section. After every refresh, App::update() pushes the new values into the histories (600 samples each, HISTORY_LEN) and rebuilds the process list.

i
Why the split. Because the collectors depend on nothing but Roots, the same code runs on the real machine, on a fake tree in a test, and behind ngtop --snapshot when a screenshot is made for these manuals (ch. 10 “Testing”).

3.2 · One refresh, step by step

System::refresh() measures the time since the previous call with the real clock and passes it to refresh_with_dt(dt). Tests call refresh_with_dt directly with a chosen dt, so rates do not depend on how fast the test runs. With dt <= 0 (the first sample) every rate is zero.

OrderStepFunctionHow often
1CPU times and per-core frequencyrefresh_cpu()every cycle
2Memory, load average, uptimeread_meminfo(), read_loadavg(), read_uptime()every cycle
3All processesrefresh_processes() → read_all()every cycle; owner, command line and ids are cached
4GPUs and per-process GPU userefresh_gpu() → GpuCollector::refresh()every cycle; open DRM files are rescanned every 5th or 20th cycle
5Network interfacesnet::refresh()counters every cycle; routes and addresses every 5th
6Disk I/Odisk::refresh_io()every cycle
7FilesystemsFsMonitor (request(), poll(), snapshot())mount list and a new request every 5th cycle; answers collected every cycle
8Connectionsconn::refresh_with()only if the Network & Disks section is visible, and then every 2nd cycle
9CPU temperature, ZRAMread_temp(), read_zram()every cycle (the sensor file is found once)
Table 3.1 — What one refresh does, in order

Step 4 also adds to each process the GPU share and GPU memory it found, so the process table can sort by GPU without knowing where the number came from. Details of each step are in ch. 4 “Data sources”.

3.3 · The event loop

The loop is in event_loop() (src/ui/mod.rs). It is single-threaded and sleeps most of the time.

event_loopsrc/ui/mod.rsSystemsrc/collect/Appsrc/ui/app.rsview::drawsrc/ui/view.rsTerminalcrosstermthe tick is duecollect_conns = (section is Network & Disks)refresh()new data in placeupdate(&sys)histories, process list, viewthe screen is dirtydraw(frame, app, sys)bufferconvert_buffer(colours)flush the changed cellsFigure 3.2 — One cycle of the event loop
RuleValueWhy
First tick250 ms after startPercentages appear almost at once instead of after a whole second
IntervalINTERVALS_MS = 250, 500, 1000, 2000, 5000 ms; default 1000Keys + and - step through the list; the configuration is rounded to the nearest step (round_interval())
Longest sleep250 msSo that SIGTERM, SIGINT and SIGHUP are noticed quickly
Sleep while waiting for services100 msApp::background_active() is true while the Services section has a request in flight
Redrawonly when something changedA flag, dirty, is set by a key, mouse or resize event that changed the state, by a service answer, or by a tick. An idle ngtop does not redraw
Interval changeat onceThe next tick is brought forward if the new interval is shorter than the time left
Queued eventsall read before the next drawHolding a key does not queue a hundred redraws
Table 3.2 — The rules of the event loop

At the start, run() does everything that can fail or allocate a lot (time zone, the first sample) before it touches the terminal. Then it switches to raw mode and the alternate screen, optionally turns on the mouse, and enters the loop. How the terminal is given back, even after an error or a signal, is in 7.6 “Terminal restore”.

3.4 · Threads

ngtop starts three kinds of thread, and none of them is a pool: each exists because one call can block.

Main thread (the interface)event_loop()keys, mouse, resize, tick, drawSystem::refresh()all collectors, in sequence, hereApp / sectionsstate; poll() the answersnever waits for D-Bus, subprocesses or statvfsOne thread per mounted filesystemngtop-statvfsstatvfs() on one mount pointasked every 5th cycle; answers come back on a channelServices thread (first visit to section 5)services threadD-Bus, cgroupsshort threadsjournal, blame, action, unit filesFigure 3.3 — The threads of ngtop and how they talk (dashed arrows are mpsc channels)
Thread nameStarted byLivesDoesTalks to the interface through
(main)the processwhole runevent loop, all collectors except the ones below, drawing—
ngtop-statvfsFsMonitor::set_mounts()while its mount existsstatvfs on one mount point. A hung network mount blocks only its own threada request channel in, one shared answer channel out
ngtop-servicesClient::spawn()from the first visit to the Services section to exitexecutes Request values: unit lists, details, sessions, boot timestwo mpsc channels
ngtop-journal, ngtop-blamerun() in worker.rsone requestrun journalctl or systemd-analyze blame with a time limitthe services answer channel
ngtop-actionrun() in worker.rsone actionstart, stop, restart, enable or disable a unit. At most one at a time; a second one is refused at oncethe services answer channel
ngtop-unitfilesunit_file_states()one refreshreads the list of unit files (a slow D-Bus call) and stores it in a cachea shared cache
Table 3.3 — Threads

The pattern is always the same: the interface sends a request and goes on; later poll() takes whatever answers have arrived, without waiting (try_recv). The interface can therefore draw, scroll and take keys while systemd is slow or absent. The tests l_interfaccia_non_attende_il_backend and un_azione_lenta_non_blocca_la_coda_e_ce_ne_e_una_sola check it with a deliberately slow backend.

i
No signal thread. Signals are not handled by a thread. signal_hook installs handlers that set an atomic flag; the event loop reads the flag. See 7.6 “Terminal restore”.

3.5 · State and drawing

The interface state is one App value passed by reference to the drawing code. The pieces that are specific to a section live in their own structure, so a section can be read on its own.

StateTypeHolds
app.procProcStatedetail tab, folded branches, scroll, the status line, the signal and start-time functions (replaceable in tests)
app.rdRdStateconnection list, filter, sort, selected row, disk scroll
app.serviziServiziunit list, filter chip, search, selection, pending requests, the confirmation, the services client
app.rows / app.viewVec<Process>all processes, sorted; and the indices actually shown (after tree order, folding and filter) with their tree prefix
app.selected_pidOption<u32>the selection is kept as a pid, so it stays on the same process when the list is reordered
app.hitsHitsclick targets computed by the last draw: tab ranges, the table area, the columns. The mouse handler uses them; there is no separate hit-testing pass
Table 3.4 — The parts of App

Layout mode. LayoutMode::for_size() chooses between large and compact: compact when the terminal is narrower than 100 columns or lower than 30 rows. The mode is recomputed on every resize event; only the Overview has a different compact layout (6.2 “The Overview section”).

Drawing. view::draw() splits the screen into the top line, the body and the bottom line, asks the active section to draw the body, then draws the key bar for that section, the status line of signals and nice (which replaces the key bar for five seconds), and last the help or the dialog on top. Every widget writes into the same ratatui Buffer (ch. 5 “The UI framework”).

Chapter 4

Data sources

4.1 · How the collectors read

A collector is a function that reads a few small text files and returns a plain value. All of them follow the same four rules, which is why a missing file never becomes an error on screen.

RuleIn the code
The root is a parameterPaths are built from roots.proc, roots.sys and roots.etc. On a real machine they are /proc, /sys, /etc; in a test they point into tests/fixtures/
Reading cannot failread_text() in src/collect/mod.rs returns Option<String>: None if the file is missing or unreadable, and invalid UTF-8 is replaced, not rejected
Parsing is separate from readingEach file has a parse_*() function that takes text. The tests feed it strings, so no file is needed
Rates need two samplesA rate is (now − before) / dt. The first sample has no “before”, so its rates are zero. Counters that go backwards (a reset) are clamped with saturating_sub, never negative
Table 4.1 — The rules every collector follows
DataRead fromCollectorNeeds privileges?
CPU/proc/stat, /proc/cpuinfo, /sys/devices/system/cpu/cpuN/cpufreq/scaling_cur_freqcpu.rsno
CPU temperature/sys/class/hwmon/*/, /sys/class/thermal/sensors.rsno
Memory, swap/proc/meminfo, /sys/block/zram*/mm_statmem.rs, mod.rsno
Load, uptime/proc/loadavg, /proc/uptimeload.rsno
Processes/proc/[pid]/stat, status, cmdlineprocess.rsno
Selected process/proc/[pid]/ many filesprocdetail.rspartly (other users' io, fd, environ)
Network/proc/net/dev, route, fib_trie, if_inet6, /sys/class/net/*/, /etc/resolv.confnet.rsno
Connections/proc/net/{tcp,tcp6,udp,udp6}, /proc/[pid]/fdconn.rsowners of other users' sockets need root
Disks/proc/diskstats, /sys/block/*/disk.rsno
Filesystems/proc/self/mountinfo, statvfsdisk.rsno
GPU/sys/class/drm/card*/device/, /proc/[pid]/fdinfo/*gpu.rsother users' clients need root
Servicessystemd and login1 on D-Bus, /sys/fs/cgroup/, journalctl, systemd-analyzeservices/starting and stopping asks polkit
Time zoneTZ, /etc/localtimesrc/ui/tz.rsno
Table 4.2 — What is read, from where

4.2 · CPU, temperature

The processor view is built from two files and one directory.

QuantityFormulaWhere
Fields of a cpu lineuser nice system idle iowait irq softirq steal. The sum of the first eight is the total; idle is idle + iowait. (guest is already counted inside user, so it is not added.)parse_times()
Usage between two samplesΔtotal = totalnow − totalbefore; Δidle likewise (capped at Δtotal). usage % = (Δtotal − Δidle) × 100 / Δtotal; zero if Δtotal is zerousage_between()
Total and per coreThe cpu line gives the total, each cpuN line one core. A core that goes offline simply stops appearingparse_stat()
Frequencyscaling_cur_freq is in kHz; the value shown is kHz / 1000 MHz. Missing on some machines: “—”read_freq_mhz()
ModelThe first model name line of /proc/cpuinfoparse_model()
Logical and physical coresLogical = number of processor blocks. Physical = number of distinct pairs (physical id, core id); unknown if the file has no such lines (virtual machines, some ARM boards)parse_topology()
Table 4.3 — CPU formulas

Temperature. find_cpu_temp_path() looks once, at the first refresh, for the file to read, and the answer (even “none”) is remembered. It scans /sys/class/hwmon/hwmon* in name order and accepts the first sensor whose name is known: coretemp (label “Package id 0”), k10temp (labels “Tctl” then “Tdie”), zenpower (“Tdie” then “Tctl”) and cpu_thermal / cpu-thermal. If the wanted label is not found it falls back to temp1_input. If no hwmon matches, it looks for a thermal zone of type x86_pkg_temp. The value is in thousandths of a degree and is divided by 1000. The same read_temp() reads disk and GPU temperatures.

4.3 · Memory, swap, load

Memory comes from one file, /proc/meminfo, whose values are in kB; the parser multiplies by 1024 (a value with no unit is taken as bytes). Missing keys count as zero.

Shown asFormulaNote
UsedMemTotal − MemAvailableNot “total − free”: cache the kernel can give back is not counted as used
AvailableMemAvailableIf the kernel is too old to have it: MemFree + Buffers + Cached, at most the total
CacheCached + SReclaimablePage cache plus reclaimable kernel memory
Buffers, FreeBuffers, MemFree
Swap usedSwapTotal − SwapFree
zswapZswap (compressed size) and Zswapped (original size)Only if the kernel exposes them
DirtyDirtyPages waiting to be written
ZRAMSum, over /sys/block/zram*/mm_stat, of the first two numbers: original data size and compressed sizeThe ratio original / compressed is shown as “2.5×”. Not shown if there is no ZRAM or it is empty
Table 4.4 — Memory formulas

Load and uptime. /proc/loadavg: the first three numbers are the 1, 5 and 15 minute averages; the fourth, “running/total”, gives the number of runnable and existing scheduling entities. /proc/uptime: seconds since boot, and idle seconds summed over all cores. The tab bar shows uptime as 3d 04:12 or 04:12:05.

4.4 · Processes

Reading every process is the most expensive part of a refresh, so process::read_all() is written to do as little as possible per process. For each directory in /proc whose name is a number it reads stat every time and the other files only when needed.

Field of statIndex after the last “)”Used for
state0R, S, D, Z, T… shown in the detail
ppid1the tree
utime, stime11, 12CPU time (clock ticks)
priority, nice15, 16priority and nice in the General tab; nice is what F7 / F8 change
num_threads17thread count
starttime19start (ticks since boot): the identity of a process, with its pid
rss21resident pages; × the page size = bytes
Table 4.5 — The fields of /proc/[pid]/stat that ngtop uses

The name (comm) is in parentheses and may contain spaces and even a “)”, so parse_stat() takes the text between the first “(” and the last “)”, and counts fields after it.

FileWhen it is readWhat for
statevery cycle, every processeverything in the table above
statusfor a new process; again every 15th cycle, staggered by pidthe owner (first number of the Uid: line). If status cannot be read, the owner of the /proc/[pid] directory is used
cmdlinefor a new process, or after the name or start time changed (an exec)the full command; NUL bytes become spaces; an empty one (kernel threads) is shown as [name]
ionot in normal runsThe code path exists behind the flag want_io, which nothing turns on, so the process list has no I/O columns. The detail of the selected process reads its own io
/etc/passwdonce, at startuser ids to names; first name wins for a duplicated id
Table 4.6 — What is read for each process, and when

CPU % of a process is a share of the whole machine, from 0 to 100:

cpu% = Δ(utime + stime) ÷ hz ÷ dt ÷ ncores × 100, clamped to 0–100

where hz is the number of clock ticks per second (clock_ticks_per_second()), dt is the real time between the two refreshes, and ncores is the number of cores listed in /proc/stat. A process that fully uses one core of eight shows 12.5. The previous sample counts only if it has the same start time: if a pid was reused by a new process, its first sample shows zero instead of a wrong number.

Memory is the resident set: rss pages × page size, with the page size read once from the system.

Tree order. tree_order() puts each process under its parent, children sorted by pid, with the prefixes ├─ , └─ and │ . A process whose parent does not exist is a root; if a loop in the parent ids ever existed, the leftover processes are made roots instead of being lost or looping forever. Sorting (sort_processes()) is always stable: ties are broken by pid.

The selected process. DetailCollector::refresh() reads, only for the pid selected in the Processes section, exe and cwd (links), stat (last core, scheduling policy), status (CPU affinity, swap), cgroup, io and the number of open files. Heavy files are read only when their tab is shown: task/*/stat (Threads, with per-thread CPU %), fd and limits (Files), the socket tables (Net), environ (Env), limits (Limits) and the cgroup files (cgroup). smaps_rollup (PSS, shared, swap) can take tens of milliseconds on a large process, so it is read only on the General tab, when the process changes and then every 5th cycle. Lists are capped: 200 files (MAX_FILES) and 500 environment variables (MAX_ENV).

4.5 · Network

Network data is split into what changes every cycle (the byte counters) and what hardly ever does (routes, addresses, link speed). The second group is reread every 5th cycle.

QuantitySource and formula
Bytes in / out/proc/net/dev: field 0 after the colon is received bytes, field 8 is sent bytes
Rate(bytesnow − bytesbefore) ÷ dt, in bytes per second. Zero for a new interface
Session peakThe highest rate seen since start (key z in Network & Disks resets it)
Link up/sys/class/net/X/operstate is “up” or “unknown” (tunnels report unknown, and are active)
Speed, duplex/sys/class/net/X/speed (Mbit/s, ignored if not positive) and duplex (ignored if “unknown”)
Default route and gateway/proc/net/route: the route with destination 0 and mask 0 and the lowest metric. Values are hexadecimal, in the byte order of the machine, and converted back to dotted form
IPv4 address/proc/net/fib_trie: the “/32 host LOCAL” entries, not 127.x; an address belongs to the interface whose route has a network that contains it: (address & mask) = destination
IPv6 address/proc/net/if_inet6: the global one if there is one, otherwise the link-local
DNS serversthe nameserver lines of /etc/resolv.conf
Table 4.7 — Network quantities

The interface shown. NetInfo::pick() chooses the one the user selected, else the interface of the default route, else the first one that is not lo. NetInfo::cycle() moves to the next or previous one and skips lo when others exist.

4.6 · Connections: inode to pid

A line of /proc/net/tcp gives two addresses, the state, the queue sizes and an inode, but not the process. The process is found by looking for a descriptor that points to socket:[inode]. Doing that for every connection on every cycle would open the fd directory of every process, so the result is kept.

ItemHow it is decoded
IPv4 address8 hex digits: the 32-bit number in the byte order of the machine (to_ne_bytes), then dotted
IPv6 address32 hex digits: four 32-bit words, each in machine order; shown compressed in brackets
Porthexadecimal
TCP state01 ESTAB, 02 SYN_SENT, 03 SYN_RECV, 04 FIN_WAIT1, 05 FIN_WAIT2, 06 TIME_WAIT, 07 CLOSE, 08 CLOSE_WAIT, 09 LAST_ACK, 0A LISTEN, 0B CLOSING
UDP state01 (connected) is ESTAB; everything else is “—”
Remote “unset”address 0 and port 0 are shown as *
Queuestx_queue:rx_queue in hex
Table 4.8 — Decoding /proc/net/tcp and its relatives
Tables of sockets/proc/net/tcp tcp6 udp udp6Live inodesset of socket inodesIn the cache?Known pidfrom the cacheScan /proc/*/fdcached pids firstsocket:[N] foundcache[N] = pidNot foundretry in 5 cyclesyesnogoneFigure 4.1 — Finding the process of a connection without scanning every file descriptor every time

The algorithm of conn::refresh_with():

StepWhat happens
1Read the four tables and parse them. A malformed line is skipped
2Forget cache entries whose inode is no longer in any table
3Collect the inodes that must be found: new ones, and the ones that failed earlier only when the retry counter (RETRY_EVERY = 5 cycles) has run out
4If there are any, scan_fds() walks the processes: first the pids already in the cache, then the others; it stops as soon as every wanted inode has been found. Pid 2 (kernel threads) is skipped
5A process whose fd folder cannot be opened (permission denied, or gone) is remembered with its start time and not tried again while it is the same process
6Attach to each connection the pid from the cache, or none
Table 4.9 — The inode to pid search
i
Cost. The connections are collected only while Network & Disks is the visible section, and then only on every second refresh (ch. 8 “Performance”). Where the owner cannot be known (a socket of another user without root, a kernel socket) the process column shows “—”.

4.7 · Disks and filesystems

The disk view combines a kernel counter file, a few static files and the mount table.

QuantityFormula (two samples of /proc/diskstats, interval dt)
Which devicesWhole physical disks only: names starting with loop, ram, zram, dm-, md, sr or fd are skipped; a name is a disk if /sys/block/NAME exists (partitions live inside the disk's folder, not in /sys/block). Without /sys/block the name is judged by pattern (sda1, nvme0n1p2, mmcblk0p1 are partitions)
Throughputsectors × 512; rate = Δbytes ÷ dt, separately for read and write
IOPSΔ(reads completed) ÷ dt and Δ(writes completed) ÷ dt
Busy timeutil = Δio_ticks ÷ (dt × 1000) (the field is milliseconds spent with I/O in progress), clamped to 0–1
Latency(Δread_ticks + Δwrite_ticks) ÷ (Δreads + Δwrites) in ms: the average time per operation. No latency if no operation happened
Queuethe “I/Os currently in progress” field, as it is now
Kindnvme… is NVMe; otherwise queue/rotational = 1 is HDD, 0 is SSD, missing is unknown
Model, sizedevice/model with spaces squeezed; size × 512. Read once per disk
Disk temperaturedevice/hwmon/hwmon*/temp1_input (NVMe, and SATA with drivetemp); found once
Table 4.10 — Disk formulas

Filesystems. parse_mountinfo() keeps only the mounts of real filesystem types (ext2/3/4, btrfs, xfs, f2fs, vfat, exfat, zfs, nfs, nfs4, cifs, smb3, ntfs, ntfs3, fuseblk, jfs, reiserfs, bcachefs, hfsplus), skips the mounts under Docker, Podman and snap folders, and drops duplicates (same mount point, or same device and same root: bind mounts of one subvolume). Octal escapes such as \040 in paths are decoded. The disk of a mount is found from its source (disk_of_source()): /dev/nvme0n1p2 is nvme0n1; device-mapper and network sources have none.

QuantityFormula (statvfs)
Totalf_blocks × f_frsize
Used(f_blocks − f_bfree) × f_frsize
Availablef_bavail × f_frsize (what an ordinary user may still write)
Fraction usedused ÷ (used + available), the same as df, so the bar fills to 100 % when an ordinary user can write no more, even if the root reserve is free
Inodes(files − ffree) ÷ files, absent if the filesystem has no inode count
Table 4.11 — Filesystem formulas

Why threads. statvfs on a network mount whose server has vanished can block for minutes, and nothing can interrupt it. FsMonitor therefore starts one thread per mount point ngtop-statvfs. Every 5th cycle the interface asks each one for a new measurement, but only those with no request in flight; it never queues a second. It collects answers that have already arrived without waiting (only the very first refresh waits, at most 300 ms, so the first screen is not empty). A mount with no answer for more than 5 seconds (FS_TIMEOUT) is shown as unresponsive and the others are unaffected. A mount that disappears from the list has its thread closed.

4.8 · GPU: devices

GPU data comes from two places: the device's own files in sysfs, and, for who uses the GPU, the fdinfo file of every open GPU descriptor of every process.

Cards/sys/class/drm/cardNDriver known?Suspended?Read sysfsusage, VRAM, clocks, hwmonUnsupportedname only (NVIDIA)Discrete and asleepno metrics are readyesnonoyesFigure 4.2 — What the GPU collector does with each card, before it reads anything
StateWhenWhat ngtop does
Activethe card is awakereads everything
Idlean integrated GPU whose runtime power management says “suspended”reads it normally: it shares the processor's package, so there is nothing to save and nothing to wake
Suspendeda discrete GPU with power/runtime_status “suspended” or power_state “D3cold”reads none of its usage, memory, clock, sensor or link files, so it is not woken; the screen says it is asleep and whether it is in D3cold
Unsupportedany other driver, such as nvidiashows the name; nothing else
Table 4.12 — The four states of a GPU

Identity. Read once per card (read_static()): vendor and device from device/vendor and device/device; PCI slot and driver from device/uevent (PCI_SLOT_NAME, DRIVER); the name from product_name if the card has it, otherwise from the pci.ids database (pci_name(): the bracketed commercial name, with the maker in front), otherwise “AMD GPU [1002:xxxx]”. The PCI slot is the key of the device everywhere in the program.

DriverUsage %MemoryClocksOther
amdgpugpu_busy_percentmem_info_vram_used / total, mem_info_gtt_used / totalpp_dpm_sclk and pp_dpm_mclk: the line with “*” is the current clock, the largest is the maximumintegrated if the VRAM is 1 GiB or less (unified memory); hwmon temperatures, power, fan; PCIe link
i915the busiest engine, from fdinfosystem memory resident for GPU clients (fdinfo)gt_act_freq_mhz (else gt_cur_freq_mhz), max gt_max_freq_mhzintegrated if the PCI slot starts with 0000:00:; otherwise a discrete Arc card, with PCIe link
xethe busiest engine, from fdinfoas i915tile0/gt0/freq0/act_freq (else cur_freq), max max_freqonly tile 0, gt 0 is read
Table 4.13 — What is read per driver

The temperature shown is the hwmon “junction” sensor, else “edge”, else the first one. Power is power1_average (else power1_input), in microwatts divided by 106; the fan is fan1_input in RPM, or pwm1 ÷ 255 as a percentage. The PCIe text “4.0 ×16” is built from current_link_speed in GT/s (2, 5, 8, 16, 32, 64 map to PCIe 1.0 to 6.0) and current_link_width.

4.9 · GPU: processes and the fdinfo files

Modern kernels (amdgpu, i915, xe) publish, for each open GPU descriptor, a file /proc/[pid]/fdinfo/N with the DRM client's counters. That is the only source of “who is using the GPU”, and it needs no special library.

Key in fdinfoMeaningUsed as
drm-driverdrm-pdevthe card the client works on
drm-client-idthe client's number, unique on that cardwith drm-pdev, the client's identity
drm-engine-NAMEnanoseconds of work done on engine NAME, cumulative (amdgpu, i915)engine busy %
drm-engine-capacity-NAMEhow many identical engines NAME stands fordivisor of the busy %
drm-cycles-NAME, drm-total-cycles-NAMEcycles used and cycles elapsed (xe)engine busy %
drm-memory-vram, drm-resident-vram* / local*video memory of the clientVRAM; resident is preferred when present, and the two families are never added
drm-memory-gtt, drm-resident-system*system memory the GPU mapsGTT; memory shown for integrated GPUs
Table 4.14 — The fdinfo keys ngtop reads

Finding the files. A process's GPU descriptors are the ones in /proc/[pid]/fd that point to /dev/dri/…. Listing them for every process every cycle would be slow, so the answer is cached per pid with the process's start time. A new pid is scanned at once; a process that has GPU descriptors is rescanned every 5th cycle, one without every 20th (both staggered by pid so not all happen together); a process whose fd folder was denied is not scanned again until it is a different process. Kernel threads are never scanned. Then only the known descriptors are read, each cycle.

One client, many processes. A DRM client can be reachable from several processes at once (a parent and its forked children share the descriptor). If each were counted, one workload would appear many times. The collector walks pids in ascending order and keeps the first process that shows a given (card, client id); the others are ignored. The client is attributed to the lowest pid that holds it.

QuantityFormula
Engine busy % of a clientTime counters: Δns ÷ (dt × 109) ÷ capacity × 100. Cycle counters: Δused ÷ Δtotal × 100. Clamped to 0–100. A client seen for the first time has no “before”: zero
Engine kindBy the name prefix: enc encode; dec, vcn, jpeg decode; video, vcs, vecs video; compute, ccs compute; copy, dma, bcs, sdma copy; anything else (gfx, render) is graphics
GPU % of a processThe busiest of its graphics, compute and copy engines, after adding up all its clients on each engine, at most 100. It is the engine that limits the work, so sums of different engines are not used
Encode and decode % of a processThe sums of its encode engines, and of its decode and video engines, at most 100
GPU memory of a processVRAM if the client has any, otherwise its resident system memory
GPU % shown in the process tableThe sum over the cards the process uses, at most 100
Usage of a cardamdgpu: the file gpu_busy_percent. If it is missing, and for Intel always, the busiest engine over all clients
Kind of workGraphics if any graphics engine has ever worked, else compute, video, copy; none if all are zero
Table 4.15 — GPU formulas

4.10 · Services, sessions, boot

The Services section needs systemd. It is available only if /run/systemd/system exists (systemd_present()); otherwise the section says so. All the following happens in the services thread (3.4 “Threads”).

DataHowNotes
List of unitsListUnits on the systemd manager, system or session bussorted by name, case-insensitive
Enabled / disabledListUnitFiles: the unit file state of each nameslow; cached for 60 s (UNIT_FILES_TTL), refreshed in the background; instances of a template (foo@bar.service) take the state of foo@.service
Main pid, cgroupGet of MainPID and ControlGroup, only for active servicestwo small calls instead of GetAll, which is costly for systemd; the cgroup is kept while the unit lives, the pid is asked again when the sub-state changes or after 10 s
Memory and CPU of a unitstraight from the cgroup: memory.current, memory.peak, cpu.stat (usage_usec)no D-Bus call per row. CPU % = Δusage_usec ÷ 106 ÷ dt ÷ ncores × 100; shown after the second reading
Detail of a unitGetAll on the Unit interface and on the Service / Socket / Mount / Swap / Scope interfacedescription, states, since when, restarts, memory, tasks, dependencies
Sessionslogin1 ListSessions and GetAll per sessionuser, seat, tty, type, host, since
Boot timesthe manager's Firmware/Loader/InitRD/Userspace/FinishTimestampMonotonicboot_phases(): firmware = firmware − loader; loader; kernel = initrd (or userspace); initrd = userspace − initrd; services = finish − userspace
Slowest unitssystemd-analyze blame as a subprocessparse_blame() reads “1min 2.5s”, “696ms”
Journaljournalctl -u NAME -n 50 -o json as a subprocess (--user-unit for the user scope)15 s limit; parse_journal_json() skips bad lines, decodes a byte-array message, and sorts by time
Table 4.16 — How the Services data is obtained

Bus and timeouts. The system scope uses the system bus, the user scope the session bus. Queries wait at most 10 seconds (QUERY_TIMEOUT); actions use a separate connection that waits up to 120 seconds (ACTION_TIMEOUT) because polkit may be waiting for a password. Subprocesses are run by run_with_timeout(), which polls every 20 ms and kills the child at the limit.

!
Subprocesses. journalctl and systemd-analyze are the only external programs ngtop runs. If one is missing, the message “journalctl not found” appears in the journal panel; the rest keeps working.
Chapter 5

The UI framework

5.1 · ratatui and the frame

ngtop uses ratatui 0.30 for what it does well — a screen as a grid of cells, a layout engine, a test backend — and crossterm 0.29 for the terminal. It does not use ratatui's ready-made widgets. Every visible element is a small struct in src/ui/widgets/ with a render(area, buf) method that writes cells straight into the ratatui Buffer. This costs a little code and buys exact control over characters, colours and widths, the same on every terminal.

Layoutsplit a RectWidget.renderwrites cellsBufferthe whole frameconvert_buffercolour depthcrosstermchanged cells onlyFigure 5.1 — From a rectangle to the terminal: the only place where colours are converted is after drawing

Drawing is a pure function of App and System. It happens inside Terminal::draw, and ratatui sends the terminal only the cells that changed since the last frame. The same function runs on a ratatui TestBackend in src/ui/snapshot.rs: render_app() returns the buffer, to_text() turns it into lines of text (empty meter cells become “·” so they can be told from full ones) and to_ansi() into text with truecolor escape sequences. This is what ngtop --snapshot WxH prints.

5.2 · Safe writing and widths

Terminals have no exceptions, but the code that writes to a ratatui buffer does: setting a cell outside the buffer panics. A monitor must survive being resized to one row, or to an enormous size. So no widget calls ratatui's writing functions directly.

FunctionFileRule
put(buf, x, y, text, style)widgets/mod.rsWrites nothing if the position is outside the buffer; clips at the right edge
putn(…, n, …)widgets/mod.rsThe same, at most n cells
clip(s, max)widgets/mod.rsCuts a string to max cells, without splitting a wide character
width(s)widgets/mod.rsWidth in cells with unicode-width, not the number of characters or bytes
style_area()widgets/mod.rsApplies a style only to the part of a rectangle inside the buffer
column_spans()widgets/proctable.rsColumn positions from the widths; the flexible column gets the rest; nothing exceeds the area
Table 5.1 — The safe writing functions

Unicode widths. A character can occupy zero, one or two cells (CJK, many emoji; combining marks take none). Process names and command lines come from the system and can contain anything. Wherever a text is placed in a column (put_line() in proctable.rs), ngtop advances by the width of each character and skips zero-width ones; a wide character that does not fit whole in the column is not drawn, so it can never spill into the next. The characters ngtop draws itself — braille, “■”, block elements, box lines, “▼”, “▲” — are one cell wide.

Arithmetic. Sizes are u16. Wherever a product could overflow on a huge terminal or a machine with thousands of cores, the code widens to u32 first (for example core_layout()) or uses saturating_sub and saturating_add.

i
Guard. The test tutte_le_misure_e_sezioni_senza_panic draws every section at 81 combinations of width (1 to 182) and height (1 to 52), including one-cell terminals, and renders each to ANSI. It is the guard of this chapter (ch. 10 “Testing”).

5.3 · The widgets

All widgets are in src/ui/widgets/. Each takes the data to show and a rectangle.

WidgetDataWhat it draws
Paneltitle, right text, bottom text, colourA rounded border (╭ ╮ ╰ ╯ ─ │) in the given colour. The title is bold on the top edge, the right text dim at the right of the top edge, the bottom text dim on the bottom edge; all clipped to leave the corners. Returns the inner rectangle; a rectangle under 2 × 2 returns an empty one
BrailleGraphvalues 0–1, gradient, directionA line graph in braille, the newest value at the right (below)
Meterfraction 0–1, gradientA bar of “■” cells. The first round(fraction × width) cells are coloured along the gradient; the rest use the grey EMPTY. NaN and values out of range are corrected
Sparklinevalues 0–1, gradientOne row of ▁▂▃▄▅▆▇█, one per value; the level is floor(value × 8), at most 7; each block takes the gradient colour of its own value. Right-aligned; missing values are blank
ProcTablecolumns, visible rows, selection, sortA header (dim; the sort column bold with ▼ for descending or ▲ for ascending), then the rows. Cells are ratatui Line values so a cell can have several colours
TabBartab names, active index, right text“ngtop 1.0 │” then the five tabs as “ 1 Overview ”, and at the right the host, uptime, load, interval and clock if there is room. tab_ranges() gives the click areas
KeyBarpairs (key, label)htop-style bar: the key on a cyan background, the label on a dark one. Items that do not fit are left out whole; the last one (q Quit) is kept when possible
Dialogtitle, lines, buttons, selectedA centred red-bordered window; it clears its background first. Width is the longest line (at least 30) plus 4; height is the lines plus 4
Table 5.2 — The widgets

The widgets keep no state. All state (selection, scroll offset, histories) is in App.

5.4 · The braille graph

Every graph in ngtop — the CPU history, the network rate, the GPU load — is a braille graph. A braille character (U+2800 to U+28FF) has a grid of 2 × 4 dots, so one terminal cell carries two columns and four levels of the graph: eight times the resolution of the cell, with a single character.

0108021004204080dots and bits0108021004204080left 0.5, right 0.50x44 + 0xA0 = 0xE4: U+28E40108021004204080left 1.0, right 0.50x47 + 0xA0 = 0xE7: U+28E7character = U+2800 + bitsone terminal cell, two columns of the graphlevel = round(value × rows × 4)dot k of row r is lit if level > 4r + kFigure 5.2 — A braille cell: two columns, four dots, each dot a bit (0x01 to 0x80)

The algorithm is braille_rows(vals, w, h, down) in src/ui/widgets/braille.rs. The graph is w cells wide and h cells high, so it has 2w columns of 4h levels.

StepRule
1. Which valuesThe last 2w values of the series. If there are fewer, the missing ones are zero and sit at the left: the graph fills from the right as time passes
2. Level of a valuelevel = round(v × 4h), with v clamped to 0–1 (NaN is 0). Thus level dots must be lit from the base
3. Which cellText column c takes values 2c (left dots) and 2c + 1 (right dots)
4. Which dotsRows are counted from the base: row rb is the one rb cells above it. In that row, dot k (0 = nearest the base, to 3) is lit if level > 4·rb + k
5. The bitsLeft column, from the base up: 0x40, 0x04, 0x02, 0x01. Right column: 0x80, 0x20, 0x10, 0x08 (mirrored for down). The sum of the lit bits is added to U+2800
6. ColourA whole row has one colour: the gradient at (rb + 0.5) ÷ h. A tall peak therefore turns from green toward red as it climbs
Table 5.3 — The braille algorithm

With down the graph hangs from the top edge and the bit tables are swapped, which gives the mirror image used under a graph that goes up (network received and sent, GPU history). The rate graphs are given values already divided by a “round” maximum: nice_max(v, floor) in fmt.rs returns the smallest power of two that is at least the largest recent value (and at least the floor), so the scale does not jump at every cycle.

+
Check it by hand. For a graph one cell high, braille_rows(&[1.0, 0.0], 1, 1, false) is U+2847 (the whole left column) and &[0.5, 0.5] is U+28E4 (two dots from the base on both sides). The unit test caratteri_attesi checks exactly these.

5.5 · Theme and gradients

Colours are defined once, in src/ui/theme.rs, as 24-bit values. They are ten gradients: lists of two to four colour stops between which the program interpolates. They were taken from the mock-up (mockup/tui.js).

CPUMEMUSEDGPUNETUPDISKWRTEMPPROCFigure 5.3 — The ten gradients of the theme, from the lowest value (left) to the highest (right)

grad(stops, t) takes t from 0 to 1 (outside the range or NaN is brought back to 0–1), scales it to the number of segments, finds the segment and mixes the two colours channel by channel: colour = a + (b − a) × f, rounded. One stop gives a flat colour; none gives the default text colour.

ConstantValueUse
FG, DIM, WHITE#c9d1d9, #6e7681, #f0f6fctext, secondary text, emphasis
DARK, EMPTY#0b0f14, #21262dtext on a bright background; the unfilled part of a meter
RED, BLUE, CYAN#f85149, #58a6ff, #39c5cferrors and dialogs; the brand; the key bar
SEL_BG, KEY_LABEL_BG#13356a, #1c2733the selected row; the key bar labels
Table 5.4 — The other colours

Each panel has a colour taken from the gradient of its content (CPU green, memory cyan, network blue, disks orange, processes orange, GPU purple). Percentages are coloured with pct_color(), temperatures by grad(TEMP, (t − 30) ÷ 60).

5.6 · Colour depth

Most terminals can show 24-bit colour, but not all: the Linux console has 16 colours, and some terminals only 256. ngtop draws everything in truecolor and converts at the very end.

CaseMode
configuration or --256 says 256 colours256
configuration says truecolortruecolor
COLORTERM contains truecolor or 24bittruecolor
TERM is linux16 colours
TERM contains truecolor or directtruecolor
anything else256 colours
Table 5.5 — How the colour mode is chosen (ColorMode::detect, tried in this order)

In the event loop, after view::draw() and before the buffer is sent, convert_buffer() visits every cell once and converts its foreground and background. In truecolor it does nothing. It is the only place where depth is considered, so no widget needs to know about it.

ModeConversion
256xterm256(r, g, b): the xterm palette is a 6 × 6 × 6 cube (levels 0, 95, 135, 175, 215, 255, indices 16 to 231) and a 24-step grey ramp (indices 232 to 255, levels 8, 18, … 238). The colour is matched to the nearest cube point, channel by channel, and to the nearest grey (average of the channels). The one with the smaller squared distance (Δr)² + (Δg)² + (Δb)² wins
16ansi16(r, g, b, only8): the nearest of the 16 base colours by the same squared distance, using the usual xterm values (black 0,0,0 … white 255,255,255). Backgrounds use only the first eight, because the Linux console cannot show bright backgrounds
Other coloursTerminal-default colours (Color::Reset) and the 16 named colours are left alone
Table 5.6 — Colour conversion
i
Colour is never the only signal. A gradient is meant to be read, but on 16 colours it can only be red, green, yellow and so on. The program stays usable because the numbers are printed next to every bar, never only coloured.

5.7 · Layout

The interface has two layouts. LayoutMode::for_size() picks the compact one when the terminal is narrower than 100 columns or lower than 30 rows. It is recomputed at every resize. The other four sections adapt by hiding what does not fit; only the Overview is redrawn with a compact version.

QuantityRuleWhere
Left column of the Overviewwidth × left_width ÷ 100 (default 40 %), at least 60 and at most width − 40draw_big()
Height of the CPU panelgrid rows + header + gap + borders, at least 8, at most one third of the area (up to one half with more than 16 cores); never so tall that less than 10 rows remaincpu_panel_height()
Grid of coresColumns needed = cores ÷ available rows. First with a bar and a percentage in each cell; if they do not all fit, only label and percentage; if still not all, the rest is announced as “+N cores not shown”core_layout()
Heights in Network & DisksEvery block has a minimum and a wanted height; minimums are given by priority (network, connections, interfaces, disks, filesystems) while they fit, then blocks grow toward the wanted height, and any spare rows go to the highest priority oneallocate()
GPU panelsAs many columns as fit in 70 cells each; panel height is the one of the richest device; a history, engines and power row only if at least 14 rows are left below the device panelsgpu::draw()
Table columnsColumns appear only if the width allows: for example the User column from 50 cells, GPU% from 60 in Processestable_columns()
Table 5.7 — Layout rules

The compact Overview is a short header — CPU (meter, percentage, temperature, clock, and one sparkline cell per core), MEM, a GPU line if there is a GPU, NET and DSK — over a short process table (small_columns(): PID, User, CPU%, Mem, Command).

Chapter 6

The sections

6.1 · Switching sections

The five sections are the five files in src/ui/sections/. Each has the same shape: a draw() function that fills the body rectangle, and, when it has keys of its own, an on_key() that gets the key before the general handler of App::on_key(). The order matters: when the dialog is open it takes every key; then the Services section; then the filter being typed; then the Network & Disks and Processes sections; then the keys common to all.

KeyEverywhere
1 … 5, Tab, Shift-TabSwitch section
F1Help window (help_lines() in keys.rs); Esc, Enter, F1 or q closes it
+ / -Refresh interval: the next or the previous of 250, 500, 1000, 2000, 5000 ms
WSave the interval and the section to the configuration file (other lines and comments are kept)
q, F10, Ctrl-CQuit
MouseClick a tab, a row or a column header; the wheel scrolls
Table 6.1 — Keys valid in every section

6.2 · The Overview section

The first section shows the whole machine at a glance, and is the one the program starts in.

ngtop — overview — 120×36ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 ServicescpuAMD Ryzen 9 7900X 12-Core Processor · 24 threadsCPU52%68°C4.06GHzC033%C625%C1233%C1840%C167%C70%C1314%C1920%C275%C812%C1475%C2056%C380%C970%C1588%C2188%C4100%C1075%C1662%C2214%C570%C1133%C1744%C2343%mem31.6Gprocessesfilter:‹ cpu ›listUsed17.3GPID NameUserMemCPU%▼Available14.3G7200rustcalex1.29G29.9Cache11.6G5230blenderalex3.50G6.8Free2.68G6001supertuxkartalex1.82G6.0Swap262M/8.00G5811obsalex540M2.42210firefoxalex1.61G2.0gpu 0/2 (d: switch)AMD Radeon RX 68001530kwin_waylandalex410M1.6GPU85% VRAM11.8G/16.0G2305Isolated Web Coalex880M1.6Temp64°Cedge72°Cjunction Power226/255 W3040codealex1.09G1.2Clock2310/2475 MHzMem clk1000 MHz2999thunderbirdalex720M0.8Fan46%1480 RPMPCIe4.0 ×163012codealex640M0.8ENC31%3055rust-analyzeralex2.29G0.81402pipewirealex22.0M0.4netenp5s0192.168.1.201561plasmashellalex520M0.4▼ 44.2MB/s2780konsolealex150M0.4▲ 3.49MB/s1systemdroot14.0M0.0▼ tot 384G2kthreaddroot0B0.0scale 128MB/s14ksoftirqd/0root0B0.0disksR 322.1 W 163.0 MB/s86kworker/u48:2-evroot0B0.0/410G/931G412systemd-journalroot62.0M0.0/home1.28T/1.80T438systemd-udevdroot11.0M0.0/mnt3.38T/3.63T702systemd-resolvesystemd-re14.0M0.034 processes · 446 threads · sel. 1/34F1HelpF3SearchF4FilterF5TreeF6SortF9Signal+/-Interval1-5SectionsqQuit
Figure 6.1 — The Overview section in the large layout (120 × 36)
PanelShowsFrom
cpuA braille graph of total CPU use on the left. On the right: a meter with total percentage, temperature and average clock, and one meter per core (C0, C1 …). The model and the thread count are on the top border. When there are too many cores to fit, the bottom border says “+N cores not shown”cpu.rs, sensors.rs
memMeters Used, Available, Cache, Free and Swap, with a memory graph beside them when there is room. A last line adds ZRAM (original → compressed and the ratio), zswap and Dirty if presentmem.rs
gpuThe selected GPU in brief, only if there is one and there is room (key d switches card)gpu.rs
netRates of the chosen interface with a graph; its name, address and “(down)” on the border; n / N switch interfacenet.rs
disksOne line per filesystem: usage bar and activitydisk.rs
processesThe process table: PID, Name, Command, User, Thr, Mem, GPU% and VRAM (with a GPU), CPU% — columns appear as the width allowsprocess.rs
Table 6.2 — The panels of the Overview
KeyAction
Up Down PgUp PgDn Home EndMove the selection. It is kept on the same pid when the list is reordered
F6, >, <Next / previous sort column (PID, Name, User, Mem, CPU, GPU, VRAM). Clicking a header does the same
iInvert the sort direction
F5List or tree. The tree setting of the Overview is separate from that of Processes (default: list)
/, F3, F4Filter: type text; it matches the name, the command line, the user or the pid, ignoring case. Enter keeps the filter, Esc removes it
F9Send a signal to the selected process (7.1 “Signals: pidfd and start time”)
n / NNext / previous network interface
dNext GPU
Table 6.3 — Keys of the Overview

Compact layout. Under 100 columns or 30 rows, the Overview becomes a header of one-line meters (CPU with a sparkline of the cores, MEM, GPU, NET, DSK) above a table of PID, User, CPU%, Mem and Command. The bottom bar shortens to Help, Filter, Tree, Sort, Sig, Quit.

ngtop — overview — 80×24ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 ServicesCPU53%68°C4.06GHzMEM17.3G/31.6Gswp 262M/8.00GGPU92% 72°C 218W 11.8G/16.0GNET▼30.1MB/s▲3.50MB/senp5s0DSKR 382.4 W 161.8M/ 44% /home 71% /mnt 93%PID UserCPU%▼Mem Command7200alex34.71.29Grustc --crate-name big_crate --edition=2024 -C op5230alex7.33.50G/opt/blender/blender projects/studio-scene.blend6001alex6.01.82G/usr/games/supertuxkart --fullscreen2210alex2.01.61G/usr/lib/firefox/firefox3040alex2.01.09G/usr/share/code/code --type=renderer --enable-cra5811alex2.0540M/usr/bin/obs --collection streaming2305alex1.6880M/usr/lib/firefox/firefox -contentproc -isForBrows1530alex1.2410M/usr/bin/kwin_wayland --wayland-fd 7 --socket way2999alex0.8720M/usr/lib/thunderbird/thunderbird3012alex0.8640M/usr/share/code/code --no-sandbox3055alex0.82.29G/home/alex/.cargo/bin/rust-analyzer1402alex0.422.0M/usr/bin/pipewire1561alex0.4520M/usr/bin/plasmashell --no-respawn1root0.014.0M/sbin/init splash2root0.00B[kthreadd]F1Help/FilterF5TreeF6SortF9SigqQuit
Figure 6.2 — The compact layout (80 × 24)

6.3 · The GPU section

The GPU section is nvtop inside ngtop: one panel per device, a history of the selected one, and the processes that use any GPU.

ngtop — gpu — 120×36ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 Services● GPU 0 · AMD Radeon RX 680016.0G · amdgpu · 0000:03:00.0GPU92% VRAM11.8G/16.0GENC28%Temp64°Cedge72°Cjunction78°CmemPower218/255 WClock2310/2475 MHzMem clk1000 MHzFan46%1480 RPMPCIe4.0 ×16GPU 1 · AMD Radeon Graphicsintegrated · 512M · amdgpu · 0000:12:00.0GPU2% VRAM60.0M/512MTemp47°CedgeClock400/2200 MHzMem clk2400 MHzGPU 0 historyuse · VRAM · tempengines · GPU 0100%Compute50%Copy (DMA)0%50%powerPower218 W0%Limit255 WGPU processessort: ‹ cpu ›5 processes · 10.1GPID UserDEV TypeGPU% ENC% DEC%VRAMCPU%▼Mem Name5230alex0Graphics50.4003.03G6.13.50Gblender6001alex0Graphics76.0004.39G6.11.82Gsupertuxkart5811alex0Graphics15.2280900M2.4540Mobs2210alex0Graphics8.400640M2.01.61Gfirefox1530alex0Graphics3.7001.15G1.2410Mkwin_waylanddDeviceF6SortF9Signal+/-Interval1-5SectionsqQuit
Figure 6.3 — The GPU section (120 × 36)
PartShows
Device panelsOne per card, side by side if there is room (70 cells each). A cell for each available quantity: usage meter, VRAM (and GTT / system memory), ENC / DEC / VID engines, temperature, power and its cap, clocks, fan, PCIe link (current / maximum). The title has the name. A sleeping card shows its state instead (4.8 “GPU: devices”)
GPU N historyBraille graphs of usage and VRAM, temperature and power of the selected card, 600 samples
engines · GPU NBusy percentage of each engine (graphics, compute, copy, video decode and encode) as read from the clients' fdinfo
powerPresent with room: power draw against the limit
GPU processesOnly the processes using a GPU: PID, User, DEV (which card), Type of work (Graphics, Compute, Video, Copy), GPU%, ENC%, DEC%, VRAM, CPU%, Mem, Name and Command, as the width allows
Table 6.4 — The GPU section
KeyAction
dSelect the next device; the history and engines follow
F6, /, F9Sort, filter, signal: as in the Overview
Table 6.5 — Keys of the GPU section

With no GPU the section shows a panel saying “No GPU detected”. With an NVIDIA card or a card asleep, the device panel says what it can: ngtop never invents a figure for what it did not read.

6.4 · The Processes section

Processes is the process manager: a large table, and for the selected process a detail area with seven tabs.

ngtop — processes — 120×36ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 Servicesprocess treefilter:systemd · PID 1rootPID UserCPU%▼MemGPU% NameGeneralThreads 1FilesNetEnvLimits1root0.014.0M0.0▾systemd412root0.062.0M0.0systemd-journalCommand/sbin/init splash438root0.011.0M0.0systemd-udevdDirectory—702systemd-r0.014.0M0.0systemd-resolveExecutable—820root0.028.0M0.0NetworkManagerParent—835messagebu0.06.00M0.0dbus-daemonStarted2026-10-20 08:56 (3d 4h ago)905root0.07.00M0.0sshdStateS (sleeping)911root0.03.00M0.0cronPrioritynice 0prio 20sched —960polkitd0.012.0M0.0polkitdAffinity—last core —1301alex0.018.0M0.0▾systemdcgroup—1402alex0.422.0M0.0pipewireUserroot (uid 0)1403alex0.040.0M0.0wireplumberThreads11404alex0.019.0M0.0pipewire-pulse1530alex1.2410M3.7kwin_wayland1561alex0.4520M0.0plasmashell2210alex2.81.61G7.7▾firefox2305alex1.6880M0.0Isolated Web Co2340alex0.0410M0.0WebExtensions2371alex0.095.0M0.0RDD Process2780alex0.0150M0.0▾konsoleCPU0.0% of one core · scale 100%2790alex0.06.00M0.0▾bash4120alex0.019.0M0.0ngtop7190alex0.085.0M0.0▾cargo7200alex30.11.29G0.0rustc2999alex0.8720M0.0thunderbird3012alex1.2640M0.0▾codememory & I/O3040alex2.01.09G0.0codeRSS14.0MRead—3055alex0.82.29G0.0rust-analyzerPSS—Write—5230alex6.83.50G47.8blenderShared—R/s—5811alex2.4540M15.8obsSwap—W/s—6001alex4.81.82G74.1supertuxkartVRAM—34 processes · 446 threads · sel. 1/34F3SearchF4FilterF5List/TreeSpaceFold←→TabsF7/F8NiceF9Signal1-5SectionsqQuit
Figure 6.4 — The Processes section (120 × 36)
PartShows
TableThe panel “process tree” (or “process list”): PID, User, CPU%, Mem, GPU% (with a GPU), Name. The default is the tree (the other sections default to a list). The bottom border shows the number of processes and threads and the selected row; the top right shows the filter
DetailA panel titled “NAME · PID N”, with the owner on the border and the tabs inside. The General tab shows:
Detail, GeneralCommand, directory, executable, parent, start time, state, nice / priority / scheduling policy, CPU affinity and last core, cgroup, user, threads
Detail tabsGeneral, Threads, Files, Net, Env, Limits, cgroup. Only the open tab's data is read (4.4 “Processes”)
CPU, memory &amp; I/OA graph of the selected process's CPU (as a share of one core, with its scale on the border); and RSS, PSS, shared, swap, VRAM, bytes read and written and their rates
Table 6.6 — The Processes section
KeyAction
SpaceFold or unfold the branch under the selected process. Folded branches are forgotten when their process disappears
F5Tree or list
Left RightPrevious / next detail tab
[ ]Scroll the detail up / down by 3 lines (the wheel too, over the detail)
F7 / F8Nice −1 / +1 for the main thread of the selected process (7.3 “Nice”)
F9 or kSignal dialog
F3 / F4 / /Search / filter
Table 6.7 — Keys of Processes
i
Jump to process. Where another section shows a process (a connection in Network & Disks), Enter there jumps here: the folded branches above the process are opened and a filter that would hide it is removed (App::jump_to_pid()).

6.5 · The Network & Disks section

This section joins what a network monitor and a disk monitor would show on two screens.

ngtop — network — 120×36ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 Servicesinterface enp5s02.5 Gbit/s full · 192.168.1.20 · fe80::26b:6bff:fe1c:2d3a/64▼ 29.5MB/speak 52.6MB/stotal 384G▲ 3.43MB/speak 3.54MB/stotal 35.4Gscale 64.0MB/s · i: switchinterfacesi / click: selectNameIPv4 address▼▲ Statelo—16.6KB/s31.3KB/s—enp5s0192.168.1.2029.5MB/s3.43MB/s● upDNS 192.168.1.1, 9.9.9.9 · gateway 192.168.1.1connectionsTCP+UDP · LISTEN yes · 10 · sort stateProto LocalRemoteStatePID ProcessQueue rx/txTCP192.168.1.20:51122203.0.113.45:443ESTAB2210firefox0/0TCP192.168.1.20:51130198.51.100.7:443ESTAB2210firefox0/0TCP192.168.1.20:51144203.0.113.80:443ESTAB2210firefox0/0nvme0n1Samsung SSD 990 PRO 2TB · NVMe · 1.82T · 43°CsdaWDC WD40EFRX-68N · HDD · 3.64TR 332.1 MB/sR 48.2 MB/sW 151.3 MB/sW 9.8 MB/sIOPS 5313/2416IOPS 780/154Use36%Use53%Lat 0.25 msQueue 0Lat 0.25 msQueue 0filesystemsMounted on DeviceTypeUseUsedFreeTotal Inode//dev/nvme0n1p2ext444%410G521G931G5%/home/dev/nvme0n1p2ext471%1.28T535G1.80T1%/mnt/dev/sda1xfs93%3.38T260G3.63T0%iInterfacecTCP/UDPlLISTENEnterGo to processF6SortzReset peaks/totals1-5SectionsqQuit
Figure 6.5 — The Network & Disks section (120 × 36)
PanelShows
InterfaceReceived and sent rates of the chosen interface as two mirrored braille graphs, the peak of the session, totals since start, IPv4 / IPv6, link speed and duplex, gateway and DNS
interfacesEvery interface with state, address, rates; click one to select it
connectionsThe TCP and UDP sockets, with local and remote address, state, queues, pid and process name. The border shows the filter, LISTEN and the sort
One panel per diskName on the border; model, type (HDD, SSD, NVMe), size, temperature; read and write rates; IOPS; busy %; latency; queue
filesystemsMount point, device, type, usage bar, available space, inode use; “not responding” for a stuck mount
Table 6.8 — The panels of Network & Disks
KeyAction
i / INext / previous interface
cProtocols: TCP+UDP, then TCP, then UDP
lShow or hide LISTEN sockets (shown by default)
F6Sort the connections by state, proto, local, remote, pid, process, queue, in turn
zReset the session peaks and totals
EnterGo to the process of the selected connection (the Processes section)
F9Signal dialog for that process
Up Down Home EndMove in the connections
PgUp PgDnScroll the disk panels
Table 6.9 — Keys of Network & Disks

The connection list is collected only while this section is visible (4.6 “Connections: inode to pid”). The height of the blocks is shared out by allocate() (5.7 “Layout”).

6.6 · The Services section

Services is a systemd manager: it lists the units, shows the detail and the journal of the selected one, and starts, stops, restarts, enables or disables it after asking.

ngtop — services — 120×36ngtop1.01 Overview2 GPU3 Processes4 Network & Disks5 Servicessystemd unitsbluetooth.service/usr/lib/systemd/system/bAll 8Active 5Failed 2Enabled 5‹systemuser ›State● active (running)since 2023-11-14 22UnitStatePIDMemCPU%PID688 · 22 tasks · limit 4915bluetooth.service● active688——Memory96.0M (peak 212M)CPU1m 42sdocker.service● active1022——Dependsnetwork-online.target docker.socketdocker.socket● active———Restarts0Restart=alwaysfwupd.service○ inactive———journal (journalctl -u bluetooth.service)llama-swap.service✖ failed———22:13:20journal line 0snapper-timeline.timer◷ waiting———22:13:21journal line 1sshd.service● active901——22:13:22journal line 2update-notifier.service✖ failed———22:13:23journal line 322:13:24journal line 4/SearchfFilteruSystem/UsersStarttStoprRestarteEnable/DisablejJournalqQuit
Figure 6.6 — The Services section (120 × 36)
PartShows
ListFilter chips (All, Active, Failed, Enabled, Timers, Sockets, each with its count), then every unit: in the panel “systemd units”: a state symbol (● active, ◷ timer waiting, ◔ starting or stopping, ✖ failed, ○ inactive), name, description, state, whether it starts at boot, PID, memory and CPU%. Narrow terminals drop the description and the boot column first
DetailOf the selected unit: description, states and since when, main pid and its command, memory with its peak, tasks, CPU time, restarts and policy, dependencies, the unit file; and below it the last journal lines
bootWhere boot time went: firmware, loader, kernel, initrd, services; and the slowest units
users &amp; sessionsThe login sessions, with the processes and memory of each user
journal · NAMEThe journal of the selected unit in full screen
Table 6.10 — The parts of the Services section
KeyAction
/Search by name or description (Enter keeps it, Esc clears it)
fNext filter chip
uSwitch between system units and the user's own units (session bus)
s, t, rStart, stop, restart: each opens a confirmation (Cancel / Confirm)
eEnable, or disable if it is already enabled (the same confirmation)
jJournal full screen; Esc goes back
Up Down PgUp PgDn Home EndMove
Table 6.11 — Keys of Services

Layout. With 120 columns × 38 rows or more (a body of 120 × 36): list (58 %) and detail (42 %) on top, boot and sessions below. From 100 columns: list and detail side by side. Narrower: the list above and the detail below. Waiting. While a request is outstanding, the section polls the thread every 100 ms. If a request has waited 10 seconds, the status line says “systemd is not responding”. No systemd. If /run/systemd/system does not exist, the section shows a single panel: “Services unavailable: systemd not found”.

Rhythm of requests (Servizi::schedule()): the unit list every 2 s (every 0.6 s until it has been received twice); the detail and the journal only 200 ms after the selection has stopped moving (so scrolling quickly asks for nothing), the detail every 2 s and the journal every 5 s; the sessions every 10 s; boot times and the slowest units once. All requests are in 4.10 “Services, sessions, boot”.

Chapter 7

Safety

7.1 · Signals: pidfd and start time

A signal sent to a pid is dangerous for one reason: pids are reused. Between the moment the user sees a process in the list and the moment they press Enter, that process may have ended and the kernel may have given its number to another one. A naive “kill the pid” would then kill an innocent. ngtop closes this window in three ways.

DefenceHow
Identify by pid and start timeA process is the pair (pid, starttime), the field 22 of /proc/PID/stat, in clock ticks since boot. Two processes with the same pid never have the same start time
Remember, then checkThe pairs are remembered when the dialog opens; at the moment of sending, the root is checked first, each target again, and each once more inside send_signal()
Hold the process, not the numberLinux 5.3 and later give a pidfd: a descriptor that refers to one process for good. After pidfd_open(pid), the start time is read again; if it is the expected one, the descriptor is the process the user chose, and the signal goes through pidfd_send_signal, which cannot reach another process even if the pid is reused a microsecond later
Table 7.1 — Three defences against pid reuse
SignalDialogsrc/ui/sections/processi.rssend_dialog()after Enter/proc/PID/statstart timeKernelpidfd, signalwhen the dialog opensremember (pid, start) of allEnter (twice for a large subtree)targets, tree_startsstart of the rootsame as remembered?verified_subtree()for each targetpidfd_open(pid)a descriptor tied to that processstart time againsame as remembered?pidfd_send_signal(fd)Figure 7.1 — How a signal reaches only the process the user chose

send_signal() is the only function that sends a signal. Its order is deliberate: open first, check after. If it checked and then opened, the pid could change in between; opened first, the descriptor and the check refer to the same process.

SituationWhat send_signal() does
pidfd_open works, start time equals the expected onepidfd_send_signal on the descriptor
pidfd_open works, start time differsNothing is sent: “the pid now belongs to another process (reused)”
pidfd_open fails with ESRCH“the process no longer exists (ESRCH)”
pidfd_open fails for another reason (old kernel, or the syscall is not allowed)Checks the start time, then falls back to kill(). It is less strong, but the check precedes it immediately
pidfd_send_signal fails with something other than ESRCHFalls back to kill() for this pid, still right after the check
The kernel refuses (EPERM, EACCES)The error is shown as text, e.g. “permission denied (EPERM)”
Table 7.2 — The outcomes of send_signal

No C library is involved: the calls are in rustix (pidfd_open, pidfd_send_signal, kill_process, setpriority_process), which makes the raw system calls.

7.2 · The signal dialog

The dialog (SignalDialog, key F9 or k) lets the user choose a signal and whether it also goes to the descendants.

SignalNameDescription in the dialog
15SIGTERMask to quit (recommended) — first in the list, so Enter alone is the polite choice
9SIGKILLstop now, unsaved work lost
19SIGSTOPpause
18SIGCONTresume
1SIGHUPreload configuration
2SIGINTinterrupt (like Ctrl-C)
10SIGUSR1user signal 1
12SIGUSR2user signal 2
Table 7.3 — The eight signals of the dialog

Up and Down choose the signal, Space toggles “whole subtree”, Enter sends, Esc cancels. These are the rules:

RuleDetailFunction
PID 1 is never touchedNot alone, not as part of a subtree. Pressing Enter on it sends nothing and says soprotected(), targets()
ngtop itselfAllowed alone, with the warning “you are signalling ngtop itself”; skipped when inside a subtreeprotected()
Subtree of PID 1 or 2Forbidden: it would be the whole system. The box cannot be tickedsubtree_allowed()
Subtree: only still-valid nodesThe set is rebuilt at send time: a node is included if its start time is the remembered one; or, for a process that was not in the remembered tree (a new child), if its parent is included and it started no earlier than the parent. A node whose pid was reused is dropped together with all its descendantsverified_subtree()
The root firstIf the root has changed, nothing at all is sent: “process N has exited, no signal sent”send_dialog()
Second confirmationFor a subtree of more than 50 processes (BIG_SUBTREE), or containing processes of other users: Enter must be pressed twice. Changing the signal or the box cancels the first pressneeds_confirm()
Visible warningsOther users' processes, protected pids and the size of the subtree are listed in the dialogwarnings(), lines()
Table 7.4 — The rules of the signal dialog

The result appears for five seconds in the bottom line instead of the key bar: “SIGTERM sent to 1234 firefox”, “SIGTERM sent to 7 processes (subtree of …)”, “SIGKILL to 1234 x: permission denied (EPERM)”, or “…: 5 sent, 2 errors (first: …)”. If some targets have vanished the line adds “(N no longer exist)”.

!
Time passes. The dialog is open for as long as the user takes. A process list that changes under it does not matter: the pairs (pid, start time) were fixed when it opened. A process that ended and a new one with the same pid will make the dialog send nothing.

7.3 · Nice

Keys F7 and F8 lower and raise the nice value of the selected process by one.

RuleDetail
Range−20 to 19. At the limit: “nice of PID NAME already at the limit (N)”
Which threadOnly the main thread, as htop does: setpriority on a pid acts on the thread whose tid equals it. The message says “(main thread)”
Lowering needs rootRaising the value (a lower priority) is allowed to the owner. Lowering it fails with EACCES or EPERM for an ordinary user; the message says “root is needed to lower the nice value”
IdentificationUnlike signals, nice acts on the pid selected in the list with no start-time check
Table 7.5 — Nice

7.4 · Service actions

Starting and stopping a service is another action that cannot be undone by ngtop, so it has its own care.

CareHow
Always askEvery action opens a dialog with the unit, the scope (system or user), and “If needed, polkit will ask for the password.”. The default button is Cancel; Left / Right moves to Confirm; Esc or n cancels
Use the system's authorityngtop never asks for a password and never runs sudo. The D-Bus call carries the flag AllowInteractiveAuth, so polkit may open its own agent. Without an agent, the answer is a refusal and the line says: “Permission denied: a polkit agent is needed, or run ngtop with sudo”
Time limitThe action's connection waits up to 120 seconds, as long as a person may need to type a password. The status line says “waiting for authorization…” meanwhile
One at a timeWhile an action is outstanding, the keys s t r e do nothing, and a second request that reaches the thread is refused: “another action is in progress”
Not blockingThe action runs in its own thread, so the screen stays alive while polkit waits
Read errorsTexts like AccessDenied, InteractiveAuthorizationRequired or “not authorized” become “denied”; everything else is shown as received (classify_error())
Fresh stateAfter enable or disable, the cache of unit file states is aged so the next list shows the change
Table 7.6 — How service actions are protected

What ngtop does not do: it never edits unit files, never masks, never runs a command through a shell. Its only two external programs, journalctl and systemd-analyze, are read-only and get a fixed list of arguments, with the unit name as a separate argument and never through a shell.

7.5 · GPUs that must not be woken

A discrete GPU in D3cold or runtime-suspended costs almost nothing; reading its files wakes it, which can take a second and a few watts. A monitor that does this every second keeps the card on forever. The rule is therefore strict.

RuleDetail
Metrics are not readFor a discrete card whose power/runtime_status is “suspended” or whose power_state is “D3cold”, no usage, memory, clock, sensor or link file is opened. The panel shows “suspended” or “D3cold”. Test: gpu_sospesa_non_viene_interrogata
Integrated GPUs are read anywayIntel on PCI bus 0, and an AMD APU once recognised: they live in the processor's package, there is nothing to save. Their state is Idle. Test: integrata_con_runtime_pm_si_legge_comunque
Unknown AMD cards are treated as discreteThe first time, before the memory size says it is an APU, the stricter rule applies
Clients are still countedThe processes are scanned for DRM descriptors and their fdinfo files as usual: per-process counters do not come from the device's sysfs folder
NVIDIA is only detectedNo read at all besides the identity
Table 7.7 — Not waking the GPU

7.6 · Terminal restore

A terminal program that leaves the terminal in raw mode on the alternate screen, with the cursor hidden and the mouse on, ruins the user's shell. ngtop gives the terminal back in every way it can end.

How it endsWhat happens
Normal quit (q, F10, Ctrl-C)The loop ends and the guard is dropped
Error in the loop (?)The function returns; the guard is dropped on the way out
PanicA hook installed first calls restore_terminal() before the panic message is printed, so the message is readable and not swallowed by the alternate screen
SIGTERM, SIGINT, SIGHUPsignal_hook sets a flag; the loop looks at it at least every 250 ms and ends in the normal way. A second signal while the flag is set ends the process at once with status 1, in case the loop is stuck
Closed terminal windowSIGHUP: the same as above
Not a terminalIf standard input or output is not a terminal, ngtop refuses to start with a clear message (exit 2) and suggests --dump
Table 7.8 — Giving the terminal back

TerminalGuard is a value whose Drop calls restore_terminal(): it leaves the alternate screen, turns off the mouse, leaves raw mode and shows the cursor. Every step ignores errors and the function can be called several times, so the guard, the panic hook and the signal path can all run it. Everything that can fail is done before the terminal is touched: the first sample, the time zone, the configuration file.

i
Tested on eight distributions. The script scripts/test-distro.sh runs ngtop under a pseudo-terminal for three seconds, sends it SIGTERM and checks that the output contains the sequences that leave the alternate screen (ESC[?1049l) and show the cursor (ESC[?25h), that nothing says “panicked”, and that the exit status is 0, 124 or 143 (ch. 10 “Testing”).

Output to a closed pipe (for example ngtop --dump | head -1) is also safe: write_stdout() ignores write errors, so there is no panic for a broken pipe.

7.7 · Time zone without trust

To show the clock, ngtop must know the local time zone offset. The usual way is a C library function, which a static musl binary has in a poorer form, so src/ui/tz.rs does it in Rust. The data are untrusted — a file anyone may have written — so the parser is cautious.

StepRule
Which zoneThe TZ variable (a leading “:” is removed). “UTC”, “UTC0” and “GMT” are UTC. An absolute path is read as a TZif file. A name is looked up in /usr/share/zoneinfo, /usr/lib/zoneinfo and /usr/share/lib/zoneinfo, but never if it contains “..”. Otherwise it is read as a POSIX rule such as CET-1CEST,M3.5.0,M10.5.0/3. Without TZ: /etc/localtime
FailureAny error leads to UTC. Never a panic
TZif headerThe first four bytes must be “TZif”. The six counts are read (as 32-bit numbers)
Size check before any allocationThe expected size of the data is computed from the counts with checked arithmetic (checked_mul, checked_add) and must fit in the file; otherwise the file is rejected. A hostile file cannot make ngtop reserve gigabytes
Version 2 and 3The second block, with 64-bit times, is used when present, followed by the footer: a POSIX rule that says what to do after the last listed transition
Finding the offsetBinary search (partition_point) for the last transition not after the instant. Past the end the footer rule decides, which is how a zone keeps working in the years beyond the table
POSIX ruleStandard name, offset, optional daylight name, offset and the two switching rules Mm.w.d/time (month, week with 5 = last, weekday, time). The day is computed from the calendar (Hinnant's algorithm for days from civil date), including leap years
Table 7.9 — How the time zone is read

Tests: nessun_panic_su_prefissi_e_file_troncati feeds the parser every prefix of a POSIX rule, and of a real zone file; tzif_inventato_e_rotto builds files with impossible counts; posix_semplici_e_non_validi tries malformed rules.

Chapter 8

Performance

8.1 · Budget and measurements

A monitor that uses a lot of CPU gives the wrong picture of the machine it watches. ngtop's goal is to be invisible: about one percent of one core at the default 1-second refresh on a small machine, a few megabytes of memory, and a start that feels instant.

MeasureValueHow it was measured
CPU at 1 s refreshabout 1 %Intel N100, default interval. One cycle takes 8 to 11 ms (measured with ngtop --bench), once a second
Resident memoryabout 5.6 MBSame machine, as reported on the project page
Binaryabout 3.5 MBdist/ngtop, static, stripped
Start to first frame25 to 30 msngtop --bench-start: cold start and the mean of 10 starts, at 182 × 52
One cycle8 to 11 msngtop --bench (below)
Table 8.1 — The figures

What the benchmark does. ngtop --bench (hidden option, bench() in src/main.rs) sets a 182 × 52 test terminal, shows each section in turn, runs three warm-up cycles and then thirty measured ones, each with its three parts timed separately: refresh (all collectors), update (histories, process list) and draw (a whole frame into a buffer). Terminal output is not included: ratatui sends only changed cells, and that is cheap by comparison.

One run on the Intel N100 (4 cores), 23 October 2026
$ ngtop --bench
1 Overview           refresh  7.42 + update 0.17 + draw 2.91 = 10.50 ms
2 GPU                refresh  6.93 + update 0.17 + draw 2.08 =  9.19 ms
3 Processes          refresh  6.66 + update 0.40 + draw 2.15 =  9.21 ms
4 Network & Disks    refresh  8.02 + update 0.28 + draw 2.26 = 10.56 ms
5 Services           refresh  6.67 + update 0.17 + draw 1.52 =  8.37 ms
(248 processes, 1 GPU(s))
i
Compare on the same machine. The figures depend on the number of processes, because reading /proc is most of the cost. On a quiet N100 a cycle takes 7 to 8 ms. Run ngtop --bench before and after a change that could slow things down, on the same machine.

8.2 · Caches and schedules

Two thirds or more of a cycle is refresh, and most of that is reading one small file per process. Drawing a frame is 2 to 3 ms. The design follows from this: read less, not faster.

DataCache or scheduleWhere
Owner of a processRead from status only for a new process, then every 15th cycle (staggered by pid, so a few per cycle); a daemon can change user without execread_one()
Command line of a processRead once; again only if the process was replaced (new start time or name). An empty one is retriedread_one()
File readsOne buffer and one path buffer are reused for every file of every process: no allocation per fileread_into()
Open GPU descriptors of a processScanned when a pid appears, then every 5th cycle (with descriptors) or every 20th (without), staggered by pid; never again if deniedGpuCollector::refresh()
Static GPU dataName, ids, driver read once per card. The pci.ids database (large) is read only when a new card appears and freed at onceread_static(), discover()
Socket to processCache by inode, incremental; the walk of all fds stops when everything wanted is found; unreadable processes are rememberedscan_fds()
ConnectionsCollected only with the section visible, every second cycleSystem::refresh_with_dt()
Routes, addresses, DNS, link speedEvery 5th cyclenet::refresh()
Filesystem sizesEvery 5th cycle, in threads, answers collected without waitingFsMonitor
Sensor filesFound once (also for each disk); then only the value is readfind_cpu_temp_path(), find_disk_temp()
Disk model, type, sizeOnce per diskread_disk_static()
Heavy process detailOnly the open tab. smaps_rollup (tens of ms on large processes) only on the General tab and every 5th cycleDetailCollector::refresh()
ServicesUnit file states cached 60 s; cgroup path kept while the unit lives; main pid asked again every 10 s or on a sub-state change; detail and journal asked 200 ms after the selection stopsdbus.rs, Servizi::schedule()
Cgroup memory and CPURead from files, one per unit, rather than one D-Bus call per rowCgroupSampler
Table 8.2 — What is cached, and how often it is renewed

Why staggered? If every pid were rechecked in the same cycle (say every 15th), that cycle would be much slower than the others and the display would stutter. Adding the pid to the cycle counter spreads the work evenly. The same trick is used for the GPU descriptors.

8.3 · The interface side

The interface side is kept cheap too.

ChoiceEffect
Redraw only when neededA frame is drawn after a key, a mouse event, a resize, a service answer or a tick. Between ticks an idle ngtop sleeps in event::poll, waking at most four times a second to look for signals
Diff on outputratatui compares the new buffer with the previous one and sends only the changed cells; a steady screen costs few bytes
Fixed-size historiesEach series is a ring of 600 samples (History): memory does not grow with the run time. Histories of interfaces, disks and GPUs that disappear are dropped
Short intervals are explicit250 ms is the fastest the user may choose (key -); the cost is then about four times as high as at 1 s
No work for hidden sectionsThe Processes detail, the connection list and the Services thread are updated only when their section is visible
Release buildopt-level = 3, LTO, one codegen unit: the compiler sees the whole program and inlines across crates
Table 8.3 — Cheap interface

Known costs, for whoever wants to go further. App::update() clones the process vector from System on every cycle and sorts it; the tree and the filter are rebuilt each time. With thousands of processes this would be the first thing to optimise. And the whole refresh runs in the interface thread: a very slow /proc (a machine under heavy load) delays the next frame by the same amount.

Chapter 9

The static build

9.1 · Why static

ngtop is meant to be copied to a server, a container or an old workstation and run there. A normal Linux executable depends on the machine's C library, and the version it was built against must not be newer than the one it finds. A static executable carries everything it needs: it asks the kernel for services, and nothing else.

RequirementHow it is met
No dependency on the distribution's libcBuilt for x86_64-unknown-linux-musl: the musl C library is linked into the file, not loaded at run time
No C code in the buildAll dependencies are Rust crates that need no C compiler
Proof, not hopebuild.sh fails if ldd finds a dynamic dependency
Tried on old and new systemsscripts/test-distro.sh runs the file on Ubuntu 24.04, Debian 12 and 10, Fedora 41, Arch, Alpine 3.20, openSUSE Tumbleweed and CentOS 7
Table 9.1 — What “static” requires
The check
$ ldd dist/ngtop
	statically linked
$ bash build.sh
...
OK: dist/ngtop is static (3.5M)

9.2 · Dependencies: Rust only

The seven direct dependencies are all Rust. Each does one job.

CrateVersionUsed forWhere
ratatui0.30Buffer, layout and test backend for the interfacesrc/ui/
crossterm0.29Raw mode, alternate screen, keys, mouse, terminal sizesrc/ui/mod.rs
rustix1.1System calls without a C library: pidfd_open, pidfd_send_signal, setpriority, statvfs, geteuid, clock ticks, page size. On Linux x86_64 it issues the raw system calls itselfprocess.rs, disk.rs, processi.rs
zbus5D-Bus in pure Rust, with the blocking API: systemd and login1services/dbus.rs
serde_json1.0Reads journalctl -o json linesservices/mod.rs
signal-hook0.3Sets flags on SIGTERM, SIGINT, SIGHUP without unsafe code in ngtopsrc/ui/mod.rs
unicode-width0.2Width of characters in terminal cellswidgets/mod.rs
Table 9.2 — The direct dependencies

The full tree for the musl target has about 150 crates (cargo tree --target x86_64-unknown-linux-musl). The ordinary way for a crate to compile C is the cc crate in a build script; cargo tree -e normal,build -i cc --target x86_64-unknown-linux-musl finds nothing, which is the proof that none does. The crate libc is in the tree, but it only declares function signatures; with musl, the functions come from the musl library that the toolchain links in. The licences of all these crates are listed by scripts/third-party.py.

i
Rule for new crates. A new dependency is accepted only if it is Rust all the way down. Before adding one, run cargo tree -i cc and build.sh: if either shows C, reconsider (ch. 11 “Extending ngtop”).

9.3 · musl and build.sh

The default Linux toolchain links against glibc, and a program built that way cannot be copied to Alpine or to an old CentOS. For x86_64-unknown-linux-musl Rust ships its own musl and links statically by default, so no C compiler or musl package is needed on the build machine: only the target, added once with rustup target add x86_64-unknown-linux-musl.

Step of build.shWhy
set -euo pipefailStop at the first error, including inside pipelines
cd "$(dirname "$0")"Works from any folder
export PATH="$HOME/.cargo/bin:$PATH"Finds cargo even in a shell where rustup is not initialised
cargo build --release --target x86_64-unknown-linux-muslThe static build, with LTO and strip (Cargo.toml)
rm -f dist/ngtopA running ngtop would give “text file busy” on overwrite
cp target/…/ngtop dist/ngtopThe finished file is dist/ngtop
ldd dist/ngtop | grep -qE "not a dynamic executable|statically linked"The verification; on failure, the dependencies are printed and the exit status is 1
Table 9.3 — build.sh, line by line

The result is about 3.5 MB: opt-level = 3, lto = "fat", codegen-units = 1 and strip = true (no debugging symbols).

9.4 · What static takes away

Static linking with musl removes some conveniences a dynamic program takes for granted. ngtop works around each of them in its own code.

MissingConsequenceWhat ngtop does
The name service switch (NSS)A static musl program cannot ask sssd, LDAP or NIS for user namesReads /etc/passwd itself (load_users()). A user known only to a directory service is shown by its number
Time zone from libcNo localtime configured by the distributionReads TZ and TZif files in Rust (7.7 “Time zone without trust”)
Host name from libc—Reads /proc/sys/kernel/hostname
Loading shared librariesNo dlopen of a driver's libraryEverything comes from kernel files. This is the reason for the next section
Different libc behaviourmusl differs from glibc in details (locale, resolver)ngtop uses no locale and does not resolve names
Table 9.4 — What a static musl binary cannot use, and the replacement

9.5 · Why not NVML

Of the three GPU makers, AMD and Intel publish their counters in the kernel, in sysfs and in each process's fdinfo. NVIDIA's proprietary driver does not; its numbers come from NVML, the NVIDIA management library, a closed-source shared library installed with the driver. A program uses it by loading it at run time. A static musl executable has no dynamic loader, so it cannot load a library built for glibc.

ChoiceConsequence
Do not use NVML in version 1.0NVIDIA cards are detected (name, driver) and appear with the state “unsupported”; no figure is shown
Keep the single static fileThe promise “copy it anywhere, it runs” is worth more than one maker's counters in this version
Open for the futureNVIDIA support through NVML is on the list of future work. How to reconcile it with a static executable is still open
Table 9.5 — Why NVIDIA is not read
!
The promise. Anything that would bring a dynamic dependency back — an NVML binding, a crate that links a system library, a call to a tool that is not installed everywhere — goes against the central promise. Discuss it before writing it.
Chapter 10

Testing

10.1 · Kinds of test

There are two kinds of test, and a few tools to look at the screen without a terminal. Nothing needs root, a GPU, systemd or a network: everything runs from fake trees and a fake services backend.

KindWhereHow manyWhat it checks
Unit testsInside each source file, in #[cfg(test)] mod tests135 test functionsOne parser, one formula, one widget. They feed text to a parse_*() function and compare numbers
Integration teststests/*.rs42 test functionsThe collectors over a fake tree, and the views drawn from them, through the public library
Distribution testsscripts/test-distro.sh8 distributions × 4 casesThe finished static binary on real systems
Table 10.1 — The kinds of test
FileSubject
tests/collect.rsCPU, memory, load, processes (names with parentheses, tree, a reused pid), network, disks, sensors, on the two snapshots
tests/gpu.rsAMD, Intel (i915, xe), integrated and discrete, suspended, NVIDIA, no GPU; the GPU views
tests/retedischi.rsNetwork, disks and connections: collectors, 'off when not visible', drawing at every size, keys
tests/servizi.rsServices with a fake backend: drawing, filters, search, an action with its confirmation, a refusal, no systemd, no blocking
tests/ui.rsThe Overview large and compact; every section at 81 sizes; machines with 1 to 128 cores
tests/cli.rsThe program as a process: it refuses to start without a terminal (exit 2); --help and --version work without one
Table 10.2 — The integration test files

10.2 · Fixtures and two samples

A rate needs two samples, so the fixtures come in pairs: A and B, as if taken one second apart. A test loads A, refreshes with dt = 0, swaps the roots to B and refreshes with dt = 1.0. Then every number is exact and nothing depends on the real clock.

tests/collect.rs
fn sistema() -> System {
    let mut s = System::new(roots("snap_a"));
    s.clock_ticks = 100;
    s.page_size = 4096;
    s.refresh_with_dt(0.0);      // first sample: rates are zero
    s.roots = roots("snap_b");
    s.refresh_with_dt(1.0);      // one second later
    s
}

Take the CPU. The cpu line of snapshot A is 1000 0 500 8000 500 0 0 0 0 0, of B 1100 0 550 8050 500 0 0 0 0 0. Totals: 10 000 and 10 200, so Δtotal = 200. Idle (idle + iowait): 8 500 and 8 550, so Δidle = 50. Usage = (200 − 50) × 100 ÷ 200 = 75 %, which is exactly what the test asserts. For a process, 80 ticks at 100 Hz in 1 s on 2 cores is 40 %.

Folder in tests/fixtures/Contents
snap_a, snap_bA reduced proc, sys and etc: the files ngtop reads, with up to seven processes (one named “a) b” to test the parser; one without permission on io; one owned by a user known only by number), two cores, a zram device, a mount table, network counters
gpu/*One tree per case: amd_a and amd_b (two samples), i915_a, i915_b, i915_idle, i915_arc_a and i915_arc_b (a discrete Intel), xe_a, xe_b, susp and susp_rpm (suspended: D3cold and runtime), nvidia, none
rete_dischi/a, bInterfaces, routes, connections with their sockets, disk counters and mounts
cgroup/a, bcgroup files for the Services CPU% and memory
Table 10.3 — The fixtures
i
Making a new one. The fixtures are plain text files copied from a real machine and then reduced. To add a case, copy the files of a real system into a new folder, cut it to what ngtop reads, and write the expected numbers by hand from the formulas in ch. 4 “Data sources”.

10.3 · Seeing the screen: snapshot, dump, sweep

Drawing is tested in two ways: by looking at its text, and by trying to break it.

ToolWhat it does
ngtop --snapshot WxH [--section N] [--ansi]Collects about 20 samples 100 ms apart; with --roots (fixtures) the graph histories are then replaced by about 300 synthetic samples around the last values (synthesize_history()), so the figures of the manuals have full graphs that are drawn, not measured. It draws one frame at the given size on a test backend and prints it, as text or, with --ansi, with truecolor escape codes. N is 1 to 5. This is how the screens in these manuals are made
--roots DIRHidden option: read DIR/proc, DIR/sys and DIR/etc instead of the machine. With a fake tree, the screenshot contains no real names, addresses or processes. With it, the Services section of a snapshot uses the fake backend (demo units, no D-Bus)
--doc-jsonHidden option: the keys and the command-line options as JSON, produced from the same tables and the same help text the program uses, so the key and option reference of the manuals cannot differ from the program
--dump [--tree]A text summary and no interface: CPU and cores, memory, swap, load, uptime, the 15 busiest processes, optionally as a tree. Useful over a bad link, in a bug report, and in CI
--benchTimes a cycle per section (8.1 “Budget and measurements”)
--bench-startTimes the start up to the first frame
Table 10.4 — Looking at the screen without a terminal
Two ways to draw one frame
$ ngtop --snapshot 80x24 --section 2
$ ngtop --roots tests/fixtures/gpu/amd_a --snapshot 120x36 --section 2 --ansi

The sweep. tutte_le_misure_e_sezioni_senza_panic draws all five sections at every combination of widths 1, 5, 12, 40, 80, 99, 100, 101, 182 and heights 1, 3, 5, 12, 24, 29, 30, 31, 52, and converts each frame to ANSI. The values straddle the thresholds (99 and 100, 29 and 30) and include terminals so small nothing fits. The same is done in other files for narrower sets of sizes: the GPU and Overview views over six GPU fixtures (tasto_d_cambia_dispositivo_e_sweep), Network & Disks (rendering_a_ogni_misura), Services (rendering_a_quattro_misure_senza_panic) and every Processes tab (disegno_senza_panic_alle_misure_previste). molti_core_si_adattano builds machines with 1, 4, 24, 64 and 128 cores and checks that the frame has exactly the requested number of lines and that the last core is present — or, at 128 cores in a small window, that “not shown” is.

10.4 · Tests that guard the promises

Most of ngtop's safety is a promise about when something happens. These tests make the promise checkable.

PromiseTest
The interface never waits for the services threadl_interfaccia_non_attende_il_backend and tenendo_premuto_giu_non_partono_richieste_a_raffica in tests/servizi.rs: a backend that sleeps; a held key; the number of requests is counted
Only one service action at a timeun_azione_lenta_non_blocca_la_coda_e_ce_ne_e_una_sola (worker.rs)
A reused pid never receives a signalpid_riusato_non_riceve_nulla, radice_riusata_i_figli_del_nuovo_non_ricevono, nodo_intermedio_riusato_salta_il_ramo (processi.rs)
PID 1 and ngtop itselfpid_uno_e_se_stessi_protetti, sottoalbero_di_init_vietato
A large subtree needs a second Entersottoalbero_grande_chiede_seconda_conferma
A sleeping GPU is not readgpu_sospesa_non_viene_interrogata, integrata_con_runtime_pm_si_legge_comunque (tests/gpu.rs)
Connections are collected only when shownla_raccolta_connessioni_e_spenta_se_la_sezione_non_e_visibile (tests/retedischi.rs)
No terminal, no interfacesenza_terminale_non_apre_la_tui (tests/cli.rs)
Hostile time zone datanessun_panic_su_prefissi_e_file_troncati, tzif_inventato_e_rotto (tz.rs)
Stuck mounts do not stop the othersstatvfs_in_thread_non_blocca_e_segnala_non_risponde (disk.rs): a fake probe that blocks
A real signal workssegnale_reale_a_un_figlio starts a child process, signals it through the whole path and checks it died
Table 10.5 — Promises and the tests that guard them

10.5 · Distributions

scripts/test-distro.sh runs the finished binary dist/ngtop in containers, with rootless podman, on the images below. It needs only podman and the binary; the containers have no network (--network=none).

ImageWhy it is in the list
Ubuntu 24.04, Debian 12, Fedora 41Current mainstream distributions
Arch Linux, openSUSE TumbleweedRolling releases: a new kernel and new files
Alpine 3.20musl-based, no systemd, tiny
Debian 10, CentOS 7Old kernels and old libraries: the strongest test of “static”
Table 10.6 — The images tried by default
CaseCommand in the containerPass if
dumpngtop --dumpexit 0 and output not empty
snap120ngtop --snapshot 120x40the same
snap80gpungtop --snapshot 80x24 --section 2the same
interactivengtop under a pseudo-terminal (script), TERM=xterm-256color, 120 × 40, for 3 s, then SIGTERMthe output has the sequences that leave the alternate screen and show the cursor; no “panicked”; exit status 0, 124 or 143; if script is missing in the image: n/a
Table 10.7 — The four cases of each image

The script prints a table with OK or ERROR for each image and keeps a log per case in the folder given as first argument (default /tmp). Other images can be passed as further arguments.

+
Order. Run build.sh first: the test uses the binary in dist/, not a debug build.

10.6 · What to run

CommandWhen
cargo testAlways, before a change is considered finished: unit and integration tests
cargo clippy --all-targets -- -D warningsThe linter; warnings are errors
cargo fmtFormatting
build.sh then scripts/test-distro.shBefore a release, or after a change that touches system calls, files or the terminal
ngtop --benchAfter a change in a collector or in drawing (ch. 8 “Performance”)
python3 docs/sources/build.py --checkAfter a change in code that the manuals describe: it verifies the cited functions and files and that the numbers are up to date
Table 10.8 — What to run, and when
Chapter 11

Extending ngtop

11.1 · Add a collector

This tutorial adds a small collector from start to finish: the number of file handles in use on the whole system, from /proc/sys/fs/file-nr. The pattern is the one of every collector in src/collect/.

  1. Write the module src/collect/filenr.rs. A data struct; a parse_*() that takes text and never panics; a read_*() that takes a path and uses read_text(). Put the test in the same file. Start the file with the two licence lines used everywhere (ch. 12 “Conventions”).src/collect/filenr.rs
    //! Open file handles of the whole system, from /proc/sys/fs/file-nr.
    
    use std::path::Path;
    
    use super::read_text;
    
    #[derive(Debug, Clone, PartialEq, Eq, Default)]
    pub struct FileNr {
        /// Handles in use.
        pub used: u64,
        /// System-wide limit.
        pub max: u64,
    }
    
    /// "allocated  unused  max": in use = allocated - unused.
    pub fn parse_file_nr(text: &str) -> FileNr {
        let n: Vec<u64> = text
            .split_whitespace()
            .map(|x| x.parse().unwrap_or(0))
            .collect();
        let get = |i: usize| n.get(i).copied().unwrap_or(0);
        FileNr {
            used: get(0).saturating_sub(get(1)),
            max: get(2),
        }
    }
    
    pub fn read_file_nr(path: &Path) -> FileNr {
        read_text(path).map(|t| parse_file_nr(&t)).unwrap_or_default()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn file_nr() {
            let f = parse_file_nr("3456\t0\t9223372036854775807\n");
            assert_eq!((f.used, f.max), (3456, 9223372036854775807));
            assert_eq!(parse_file_nr(""), FileNr::default());
        }
    }
  2. Connect it to System. Four lines in src/collect/mod.rs. Always build the path from self.roots, never write /proc in the collector.src/collect/mod.rs
    // src/collect/mod.rs
    pub mod filenr;                                  // 1. declare the module
    
    pub struct System {
        // ...
        pub files: filenr::FileNr,                   // 2. the field
    }
    
    // in System::new():
        files: filenr::FileNr::default(),            // 3. its initial value
    
    // in System::refresh_with_dt():
        self.files = filenr::read_file_nr(           // 4. read it, through the roots
            &self.roots.proc.join("sys/fs/file-nr"),
        );
  3. Decide how often. One small file can be read every cycle. A costly one should be read every 5th cycle, like routes (refresh_count.is_multiple_of(5)), or only when a section is visible, like connections (a collect_* flag, set in event_loop(), snapshot() and bench()). See 8.2 “Caches and schedules”.
  4. Add the file to the fixtures. Put proc/sys/fs/file-nr in tests/fixtures/snap_a/ and snap_b/, with different numbers if it is a rate, then a test in tests/collect.rs that asserts the value after the two samples (10.2 “Fixtures and two samples”).
  5. Show it. The quickest place is dump() in src/main.rs: one more outln!. To show it on screen, see the next section.
  6. Run cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt, and ngtop --dump.
RuleWhy
Parse text, read pathsA parser tested with strings needs no file; the reader is two lines
Missing, empty, strange: a defaultNever unwrap() or expect() on data from the system; use unwrap_or, ? on an Option, saturating_sub, checked_*
A rate needs a previous sampleKeep it in System (like prev_cpu) and use the dt that refresh_with_dt() is given, not the clock
Keep what is slow out of the interface threadIf a read can block (a network file system, a device that wakes up), do it in a thread, as FsMonitor does
Never wake hardwareIf the data comes from a device that sleeps, learn from 7.5 “GPUs that must not be woken” first
Table 11.1 — Rules for a new collector

11.2 · Add a panel or a section

A new panel inside a section, or a new section, is only drawing. The data is already in System and App.

  1. Draw with a Panel. Panel::new(title, colour).right(text).render(area, buf) returns the inner rectangle. Inside, write with put() and putn(), never with ratatui's own functions (5.2 “Safe writing and widths”). Use Meter for bars and BrailleGraph for histories; take the colour from a gradient in theme.rs.
  2. Check the size first. Return at once if inner.width or inner.height is too small for your content, as the existing panels do (if inner.width < 20 || inner.height < 3 { return; }).
  3. Place it in the layout of the section's draw() (a Layout::vertical or horizontal of the existing code). Decide what gives way on a small terminal; hide the panel rather than squeeze it.
  4. If it needs history, add a History to App and push to it in App::update().
  5. If it has keys, handle them in the section's on_key(), and add them to the key table in src/ui/keys.rs: the bottom bar, and the lines of the F1 help.
  6. Test that the section still draws at every size: the sweep in tests/ui.rs covers all sections automatically.

A whole new section is the same work plus a list of places that know there are five of them. Search the source for Section::ALL, Section::Servizi and the text “1-5”:

WhereWhat to change
src/ui/sections/NEW.rs, sections/mod.rsThe file with draw() and, if needed, its state struct; a pub mod line
Section in src/ui/app.rsA variant; add it to ALL (the array length changes) and to title()
view::draw() in src/ui/view.rsA branch in the match, and the choice of key bar
src/ui/keys.rsA KEYS_… table; add it to section_bars() (its array length) and one entry to SECTION_HELP, in the same order as Section::ALL; “1-5” becomes “1-6” everywhere
App::on_key()The digit pattern '1'..='5' that selects a section
src/config.rsThe range check (1..=5) of --section, the text of USAGE, and the tests that assume the last section
App fields and App::new()The state of the new section
event_loop(), snapshot(), bench()If the section needs a collect_* flag in System, set it in the three places
README.md and the manualsThe list of sections and keys
Table 11.2 — The places that know the number of sections
!
One source for keys. The key table, the F1 help and the manuals' key reference come from the same source (src/ui/keys.rs, used by --doc-json). Change keys there and only there.

11.3 · Where things go

A short guide to deciding where something belongs.

I want to…Where
Read a new kernel fileA collector in src/collect/; 4.1 “How the collectors read”
Show a number differentlyThe section's drawing function; formatting helpers are in src/ui/fmt.rs
Add a colour or gradientsrc/ui/theme.rs. Check the result on 256 and 16 colours (5.6 “Colour depth”)
Change a refresh rhythmThe condition on refresh_count in the collector or in System::refresh_with_dt(); update 8.2 “Caches and schedules”
Add an option or a settingsrc/config.rs: Cli, Config, parse_full(), merge(), USAGE; it appears in the manuals through --doc-json
Support another GPU driversrc/collect/gpu.rs: a branch in read_dynamic(), and its engines' names in engine_kind()
Support another init system than systemdA new implementation of the Backend trait in src/collect/services/ (the fake one in fake.rs shows the minimum) and a choice in Servizi::start()
Add a new dependencyOnly a pure Rust crate (9.2 “Dependencies: Rust only”)
Table 11.3 — Where does it go?
+
Actions. Before adding anything that acts on the system (a signal, an action, a write), read ch. 7 “Safety”. The three habits: identify the target twice, ask before doing, never wait in the interface thread.
Chapter 12

Conventions

12.1 · Language, names and headers

The rules below are the ones the code already follows. Where a rule can be checked by a tool, the tool is named.

RuleDetailChecked by
Text on screen is EnglishPanel titles, key labels, dialogs, status lines, messages, --helpreading; the manuals check their own text
Comments are short and in ItalianOne or two lines above the function or the odd line, saying why, not what—
Documentation is EnglishThese manuals, README.md, decisions-and-history.mdbuild.py --check flags Italian sentences in the manuals
NamesTypes, functions and variables are English; test names are Italian sentences with underscores; the modules and variants of the sections keep their Italian names (see the glossary)—
Licence headerEvery source file starts with two lines: // Copyright (c) 2026 Nicola Fiorillo and // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0. In scripts they follow the shebang linereading
Formattingcargo fmt, default rustfmt settingscargo fmt --check
LintsNo clippy warning; the few #[allow(...)] are local and explainedcargo clippy --all-targets -- -D warnings
Table 12.1 — Language, names and headers
The start of every .rs file
// Copyright (c) 2026 Nicola Fiorillo
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0

//! Memoria e swap da /proc/meminfo (valori in byte).

12.2 · Rules for robust code

A monitor runs for weeks beside everything else on a machine. It must not be the thing that crashes. These rules are applied everywhere in src/.

RuleIn practice
No panic on system dataOutside tests, the sources contain no unwrap(), no expect(…), no panic!, no unreachable!, no todo! and no unsafe. (Tests may use them.) A grep of src/ for them, ignoring test modules, should find nothing
Missing or unreadable is not an errorA file that is missing, or a field that cannot be parsed, gives None, a default or a dash on screen. Permission denied is the same: the value is “—”
Arithmetic cannot overflow or go negativesaturating_sub for counters, saturating_mul for sizes, checked_* in the time zone parser, u32 for products of screen sizes, and clamp for every shown percentage
Division is guardedA rate is computed only when dt > 0; a ratio only when the denominator is non-zero; max(1) on core counts and tick rates
Draw inside the bufferOnly put() and putn() write cells; they ignore anything outside the buffer
Paths from the rootsCollectors take their paths from Roots (the literals are the defaults in Roots::default()). The Services code has two parameters of its own, run_dir and cgroup_root. Still literal: the zoneinfo folders, and one read of /proc/PID/comm in the services thread
Bound the dataLists from the system are capped (200 open files, 500 environment variables, 50 journal lines), and the configuration file is read only if it is a small regular file
Never block the interface threadSlow or possibly blocking work goes to a thread with a channel (3.4 “Threads”)
Do not run anything through a shellExternal programs get a fixed list of arguments
Pure Rust dependencies9.2 “Dependencies: Rust only”
Table 12.2 — Robustness rules

12.3 · Habits of the code base

Habits that keep the code uniform.

HabitDetail
Parse and read apartA function that takes text and returns a value, and one that reads the file; every parser has a unit test with a malformed input
Tests next to the codeA #[cfg(test)] mod tests at the end of the file; integration tests only for what needs a fake tree or a whole frame
Tests are deterministicNo real clock (refresh_with_dt), no real network, no real systemd (FakeBackend), no dependence on the machine's files (only tests/fixtures/, and /usr/share/zoneinfo with a fallback)
Replaceable edgesWhat is dangerous or slow is behind a function pointer or a trait so a test can substitute it: KillFn, StartFn, FsProbe, Backend
One source for each factKeys: keys.rs. Gradients: theme.rs. Options: USAGE. The manuals read them
Hidden diagnostics--roots, --bench, --bench-start, --doc-json exist for developers and are not in --help
Simple stateState is plain structs with public fields and functions; no global mutable state except the temperature unit (a single atomic in fmt.rs)
Table 12.3 — Habits
i
Checklist. Before a change is finished: cargo fmt, cargo clippy --all-targets -- -D warnings, cargo test
+
Keep the manuals true. If you change a thing the manuals describe — a key, an option, a formula, a cache rhythm, the file layout — change the manual in the same step. The file map and the numbers update themselves; the sentences do not.
Chapter 13

Glossary

13.1 · The project's own words

Words ngtop uses as terms, in the code and in this manual, with the section that explains them. Several source files keep their Italian names; the first terms tell which is which.

Collector
A function or module that reads kernel files and computes numbers; it knows nothing about the screen (src/collect/). See 3.1 “Collect and draw”.
Roots
The three folders (proc, sys, etc) every collector reads from. Real on the machine, fake in tests. See 4.1 “How the collectors read”.
System
The structure that holds the latest data of the whole machine and the previous samples for rates (src/collect/mod.rs). See 3.2 “One refresh, step by step”.
App
The state of the interface: histories, selection, filter, dialog, state of each section (src/ui/app.rs). See 3.5 “State and drawing”.
Section
One of the five main screens, selected with 1 to 5: Overview, GPU, Processes, Network & Disks, Services. In the code, the modules and variants have Italian names: panoramica / Panoramica is the Overview, processi / Processi is Processes, retedischi / ReteDischi is Network & Disks, servizi / Servizi is Services; gpu is the same in both languages. See ch. 6 “The sections”.
Large layout, compact layout
The two forms of the Overview: compact under 100 columns or 30 rows. See 5.7 “Layout”.
Tick
One cycle of the event loop that refreshes the data and redraws; its length is the refresh interval (250 ms to 5 s). See 3.3 “The event loop”.
dt
The real time, in seconds, between two refreshes. A rate is a difference of counters divided by dt; it is zero for the first sample. See 3.2 “One refresh, step by step”.
Frame, buffer
One whole screen as a grid of cells (ratatui Buffer), drawn from scratch each time and sent to the terminal as the cells that changed. See 5.1 “ratatui and the frame”.
Widget
A small struct that draws one kind of element into a buffer (Panel, Meter…). See 5.3 “The widgets”.
Panel
A bordered rectangle with a title on its border; the unit of every section. See 5.3 “The widgets”.
Braille cell
One character U+2800 to U+28FF: a 2 × 4 grid of dots, used to draw graphs at eight times the cell resolution. See 5.4 “The braille graph”.
Gradient
A list of colour stops between which the program interpolates; every bar, graph and percentage takes its colour from one. See 5.5 “Theme and gradients”.
Truecolor, 256, 16
The three colour depths of a terminal; ngtop draws in truecolor and converts the frame at the end. See 5.6 “Colour depth”.
Alternate screen
A second terminal screen that a full-screen program draws on and then leaves, so the shell's text comes back intact. See 7.6 “Terminal restore”.
Raw mode
Terminal mode in which keys reach the program without being edited or echoed. See 7.6 “Terminal restore”.
Fixture
A fake /proc, /sys, /etc tree in tests/fixtures/ used by tests and by documentation screenshots. See 10.2 “Fixtures and two samples”.
Snapshot
One frame drawn on a test backend and printed as text or ANSI: ngtop --snapshot WxH. See 10.3 “Seeing the screen: snapshot, dump, sweep”.
Sweep
A test that draws every section at many sizes, including absurd ones, to prove there is no panic. See 10.3 “Seeing the screen: snapshot, dump, sweep”.
Backend (services)
The trait through which the Services section talks to systemd. The real one uses D-Bus; the fake one gives demo data. See 3.4 “Threads”, 4.10 “Services, sessions, boot”.
Worker thread
The thread that executes the slow requests of the Services section while the interface keeps running. See 3.4 “Threads”.

13.2 · Linux and system terms

Terms of the system that ngtop reads, and what they mean for it.

amdgpu, i915, xe
The Linux kernel drivers of AMD, Intel (older) and Intel (newer) graphics. They publish counters in sysfs and in fdinfo. See 4.8 “GPU: devices”.
cgroup
The kernel's grouping of processes with its accounting. systemd puts each service in one; ngtop reads memory and CPU of a service from its cgroup files. See 4.10 “Services, sessions, boot”.
Clock tick, hz
The unit of CPU times in /proc: usually 100 per second. See 4.2 “CPU, temperature”, 4.4 “Processes”.
D3cold
The deepest power state of a PCI device. A GPU in D3cold is not read. See 7.5 “GPUs that must not be woken”.
DRM client
One user of a GPU, with its own counters, identified on a card by its client id. A process can show it, and so can its children. See 4.9 “GPU: processes and the fdinfo files”.
D-Bus, polkit
The message bus of Linux and the service that decides if an action may be done, asking for a password if necessary. See 7.4 “Service actions”.
Engine
A part of a GPU that works on one kind of task: graphics, compute, copy, video decode or encode. See 4.9 “GPU: processes and the fdinfo files”.
fdinfo
The file /proc/[pid]/fdinfo/N, with the counters of a DRM client for one open GPU descriptor. See 4.9 “GPU: processes and the fdinfo files”.
hwmon, thermal zone
The kernel's sensor interfaces under /sys/class. ngtop reads temperatures, power and fans from them. See 4.2 “CPU, temperature”.
Inode (socket)
The number in /proc/net/tcp that identifies a socket; the same number appears in socket:[N] in a process's file descriptors. See 4.6 “Connections: inode to pid”.
IOPS, latency, queue
Operations per second, average time per operation, and requests in flight, of a disk. See 4.7 “Disks and filesystems”.
musl
A small C library that can be linked into an executable completely; with it ngtop is one static file. See 9.3 “musl and build.sh”.
NSS
The name service switch of the C library, which lets programs ask directories (LDAP, sssd) for user names. A static musl program has none. See 9.4 “What static takes away”.
NVML
NVIDIA's management library; shared and closed. The reason NVIDIA cards are only detected. See 9.5 “Why not NVML”.
pidfd
A file descriptor that refers to one process for ever, immune to reuse of the pid number. See 7.1 “Signals: pidfd and start time”.
Runtime PM
Power management that suspends a device when idle. A discrete GPU suspended this way is not woken. See 7.5 “GPUs that must not be woken”.
Start time (starttime)
The moment a process started, in clock ticks since boot; with the pid, the identity of a process. See 7.1 “Signals: pidfd and start time”.
statvfs
The call that gives the size and free space of a filesystem; on a stuck network mount it can block, so it runs in a thread. See 4.7 “Disks and filesystems”.
TZif
The binary format of time zone files in /usr/share/zoneinfo, read by ngtop's own parser. See 7.7 “Time zone without trust”.
zswap, ZRAM
Two ways of compressing memory instead of swapping it out to disk; the Overview shows the saving. See 4.3 “Memory, swap, load”.