README.md
September 3, 2026 · View on GitHub
██╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗
██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗██╔══██╗██╔════╝
███████║ ██║ ██║ ██║██████╔╝██████╔╝███████╗
██╔══██║ ██║ ██║ ██║██╔═══╝ ██╔══██╗╚════██║
██║ ██║ ██║ ╚██████╔╝██║ ██║ ██║███████║
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝
[THE FAITHFUL RUST PORT OF HTOP]
"The htop C source is the spec — ported function-for-function, not reimagined."
htoprs is a faithful Rust port of htop, the
interactive process viewer. Every function under src/ported/ ports a specific htop
C function, cited by <File>.c:<line> in its doc comment. Created by MenkeTechnologies.
Read the Docs · Engineering Report · Port Report
Porting methodology
The C source is the specification. Ports are faithful — the original C is
translated function-for-function, never reimplemented from scratch. This is
enforced mechanically, following the same precedent as zshrs.
-
Spec: htop 3.5.1, 131
.cfiles. The C source is not vendored in this repo — the tools that need it read it from$HTOP_C_SOURCE, defaulting to a local checkout at~/forkedRepos/htop, and abort withERROR: htop source not found at <path>when that directory is missing (scripts/gen_port_report.py,tests/data/extract_c_fn_names.sh). The build itself needs no checkout: the C-function snapshottests/data/htop_c_fn_names.txtis committed, socargo buildand the port-purity gate work from a bare clone. -
Port tree:
src/ported/<file>.rs— one Rust module per C file. Eachfncarries a/// Port ofcitation naming its<File>.c:<line>origin. -
Extensions tree:
src/extensions/<name>.rs— htoprs-original code that is not a translation of htop C and is therefore exempt from the port-purity gate.extensions::themeholds the named color-scheme system (31 built-in 6-color palettes plus custom-theme plumbing), andextensions::overlaythe themed keyboard-help overlay, theme chooser, and theme editor (rendering into aratatui::Buffer) — both ported from iftoprs.extensions::colorsmakes a chosen theme recolor the live htop UI in 256-color via a base16-style ANSI palette remap consulted at the singleNcurses::to_colorchoke point, andextensions::prefspersists the selection to~/.config/htoprs/prefs.json. The overlay is wired intoScreenManager_run:zopens the theme (color scheme) chooser,~the editor,h/?/F1 the themed help overlay (Esccloses it),gtoggles the header,Btoggles the border. These are all keys htop leaves free — lowercasec(tag process + children) andC(setup) belong to htop, so the overlay must not shadow them; likewisebis the bar fill-style cycler below andxis htop's file-locks screen. Toggles and the bar-style change surface a transient status toast (overlay::draw_status, ported from iftoprs).extensions::bridgematerializes the live portedProcessrows as theProcmodel (viaObject::as_process), andextensions::panelsis the running-TUI wiring for the htoprs-original monitoring capabilities — the monitoring analog of the theme overlay. A thread-local state is fed the real table each sample tick (advancing the per-PID history rings, the debounced threshold alerts, the CPU history graph, and the memory-exhaustion forecast) and gets first refusal on keys, with hotkeys chosen from those htop leaves unbound:Key Capability fFuzzy process finder (Enter jumps the cursor to the match) rRegex / substring filter over comm/cmdline/user, with a saved named store ( ~/.config/htoprs/filters.json)dSnapshot: first press captures a baseline, next press diffs the live table against it ( +started-exited~changed);wwrites the snapshot JSONoExport the current table to JSON + CSV under ~/.config/htoprs/AThreshold alerts — the rule set and every currently-firing PID EForecast — per-process memory-exhaustion horizon: every rising PID's projected time to its real ceiling, soonest first ( ttoggles the inline row tint)GBraille CPU history graph (system total plus the selected PID) yAggregate/pivot: live CPU+memory totals grouped by user / command / parent ( Tabcycles the key):Command palette — fuzzy-search every action by name and run it (reuses the fmatcher); reaches both extension and htop actionsvCycle the per-PID CPU sparkline: off → narrow right-edge column → CPU-scaled inline braille graph Some of these reach the rows themselves rather than a modal, injected at the per-row draw site in
Panel_draw(the same extension-hook pattern the theme border uses, so no new portedfn): a firing-alert PID's row is recolored (red), a PID forecast to hit its memory ceiling within the horizon is tinted amber through the same single chokepoint (a live alert outranks a projected one), and thevsparkline is drawn on the rows.vcycles three states — off, a narrow braille sparkline overdrawn at each row's right edge, and an inline graph mode where each process grows a full-width braille CPU graph beneath its info line. The graph is rendered by the same braille canvas as theGhistory graph, and its height scales with the process's CPU: idle processes stay a single line, busy ones grow up toSPARK_GRAPH_Hgraph lines (more CPU = more rows). This makes the process panel variable-height:Panel_draw/Panel_onKeyproject every screen-Y, page step, and scroll clamp through per-row heights (item_height=1 + graph_lines(cpu)for a process,1otherwise), so the cursor, paging, and scrolling all track whole processes. Non-process panels keeprowHeight = 1and are byte-identical to the ported behavior. -
Memory-exhaustion forecast (
extensions::forecast,E): a leading indicator htop lacks entirely. Each refresh it fits a hand-rolled ordinary-least-squares trend on every PID's bounded memory ring and projects the wall-clock time until that PID's resident set crosses its own real ceiling — in precedence order a Linux cgroup-v2memory.max, else the process address-spaceRLIMIT_AS, else total machine RAM (detected once and cached). Only rising trends produce an ETA; the modal lists them soonest first ("pid 3120 mysqld: +12 MB/s → cap in ~4m20s"), andtenables an inline amber row tint for any PID projected to exhaust within the horizon (default 1 hour). The sample interval is measured live from the tick gap, so the ETA tracks the real refresh rate and pauses. The tint and horizon persist to~/.config/htoprs/prefs.json. -
Bar fill-glyph cycler (
extensions::barstyle, ported from storageshower):bcycles the character every bar meter (CPU, Memory, Swap, …) fills with, through five styles — Classic (|, htop's default), Gradient (position-shaded█▓▒░with a▸tip), Solid (█), Thin (▬/▸), and Ascii (#/>) — keeping each segment's semantic color. Each press shows the iftoprs-style status toast (overlay::draw_status, e.g.Bar style: solid) centered near the bottom for 3s. The selection persists to~/.config/htoprs/prefs.jsonand is restored on launch. It is consulted by the portedBarMeterMode_drawfill loop (barstyle::fill_glyph,None⇒ htop's native glyph) and wired into the keybinding table (Action_setBindingsbinds thekeys['b']slot htop leaves free unlessHAVE_BACKTRACE_SCREENis set) as anHtop_Action. -
Port-purity gate (
build.rs): on everycargo build/cargo test/cargo checkthat touchessrc/ported/, every freefnname is checked against the htop C-function snapshot attests/data/htop_c_fn_names.txt. Afnwhose name has no C counterpart fails the build. Rust-original helpers are rejected; genuine architectural helpers must be justified intests/data/fake_fn_allowlist.txt. The gate cannot be bypassed bycargo test --test X. -
C-name snapshot: regenerate with
tests/data/extract_c_fn_names.shafter pulling upstream htop (HTOP_C_SOURCE=~/forkedRepos/htop). -
Stub scaffold (
scripts/gen_port_stubs.py): lays out the full port surface — one module per C file, onetodo!()stubpub fnper C function (named to satisfy the gate). Stubs are placeholders, not ports; replace each with a faithful body and a/// Port ofcite. Already-ported modules are never overwritten. -
Port report:
scripts/gen_port_report.pywalks the C source and the Rust port and writesdocs/port_report.htmlwith per-file and overall coverage, derived from source at run time (nothing hardcoded). Atodo!()/unimplemented!()body is counted as stubbed, never ported, so scaffolding cannot inflate the coverage number.
Current state
The port covers 1068 of 1101 C functions (97.0%) across 130 of the 131 C
files, with 1 stub remaining — the interactive TUI assembles and runs on both
macOS (darwin) and Linux; it is a daily driver on macOS. The
core is ported end-to-end: the process model and table build (Process,
ProcessTable, Table, Row, Machine), the container/util layer (Vector,
Hashtable, RichString, XUtils), the full meter set (CPU, Memory, Swap,
Load, Battery, Network, DiskIO, GPU, ZFS, and the dynamic meters), the UI panels
and main loop (Panel, ScreenManager, MainPanel, FunctionBar, Header,
and the setup/columns/colors/display-options screens), key dispatch (Action,
IncSet, LineEditor), the CRT terminal layer, and the per-OS machine /
process-table backends (darwin / linux / freebsd / netbsd / openbsd /
dragonflybsd / solaris). Functions that need genuinely unportable C substrate
(the xMalloc family, raw Object**/bucket-table internals, varargs
formatters) stay intentionally unported; a shrinking set of honest todo!()
stubs marks work still in flight. Overall and per-file coverage — real ports vs
stubs — lives in docs/port_report.html (derived from source at run time —
nothing hardcoded).
On top of the port sits an src/extensions/ layer (21 modules, exempt from the
port-purity gate) — the named color-theme system, the help/theme overlay, and
the live monitoring suite (per-PID history, alerts, memory-exhaustion forecast,
braille CPU graphs, finder, diffs, exporters) described above.
Terminal backend & substrate
htoprs must render byte-for-byte identical to htop (enforced by the parity
suite). The terminal layer is crossterm —
pure-Rust, vendorable, cross-arch, no C dependency — giving full control over
every glyph/color/attribute so the output matches htop while the draw code is a
behavioral (not line-for-line) port. The substrate the UI renders through is
ported: Object.c (htop's vtable OOP → a Rust Object trait with a class-chain
Object_isA), RichString.c (the full styled-character buffer), and CRT.c's
color model — the ColorElements enum and every CRT_colorSchemes entry
transcribed verbatim so colors match htop exactly. The terminal-control fns
(CRT_init/readKey, Panel/ScreenManager draw) and the platform
data-collection layer (Platform_*, process scan) are now ported and drive the
live TUI; the remaining gaps are the un-started files tracked in the port report.
XUtils.c — string / math utilities:
| C function | notes |
|---|---|
String_cat | concatenation |
String_trim | trims leading/trailing space, tab, newline (only those three) |
String_split | splits on a separator; interior empties kept, trailing empty dropped |
String_splitFirst | splits on first separator only |
String_contains_i | case-insensitive substring; |-multi-needle mode |
compareRealNumbers | NaN-aware ordering (NaN sorts first) |
sumPositiveValues | sum of strictly-positive values, NaN skipped |
countDigits | digit count in a given base, with overflow guard |
countTrailingZeros | mod-37 lowest-set-bit table |
Vector.c — container sort / search core, ported as generics over a slice
with a C-int-returning comparator (the Object** pointer array becomes a
slice; signed isize indices preserve the C's below-left arithmetic):
| C function | notes |
|---|---|
swap | exchange two slots |
partition | Lomuto partition, pivot moved to right |
quickSort | recursive quicksort, pivot left + (right - left) / 2 |
insertionSort | in-place insertion sort over [left, right] |
Vector_indexOf | linear search, C -1 sentinel preserved |
Hashtable.c — prime-table math:
| C function | notes |
|---|---|
nextPrime | smallest OEIS prime >= n; aborts (panics) if none fits |
Meter.c — value formatting:
| C function | notes |
|---|---|
Meter_humanUnit | kibibytes → human-readable string (K…Q, inf cap) |
The C allocation wrappers, null-terminated-string helpers, varargs formatters,
and String_freeArray (XUtils.c); the Object** dynamic-array memory
machinery — Vector_new / _insert / _add / _resizeIfNecessary and the
rest (Vector.c); and the open-addressing bucket table — Hashtable_new /
_put / _get / _foreach and the rest (Hashtable.c) — have no faithful
safe-Rust analog (Rust's Vec / HashMap own allocation, bounds, probing,
and lifetimes) and are intentionally not ported. combSort is commented-out
dead code in Vector.c and is not ported.
Build & test
cargo build # runs the port-purity gate
cargo test # ports have hand-crafted unit tests pinning C edge behavior
cargo test --test parity # diff htoprs output vs the reference htop 3.5.x
python3 scripts/gen_port_report.py # regenerate docs/port_report.html
The parity suite (tests/parity/) runs the same inputs through the
reference htop (the C original htoprs is ported from) and htoprs, then
compares output byte-for-byte, modulo the deliberate rebrand
(htoprs→htop, version banner). It skips gracefully when no matching htop
3.5.x is installed, so CI stays green; a dev box with htop runs the real
comparison. Point it at a specific reference with HTOP_REF=/path/to/htop.
See docs/PARITY.md.
Troubleshooting crashes and exits
A TUI runs on the alternate screen with the tty in raw mode, so a crash report printed to the terminal is painted over and lost the moment the screen is restored. htoprs therefore persists both crashes and the reason for every exit to a logfile:
cat ~/.cache/htoprs/crash.log
Crash entries record the time, htoprs version, pid, thread, source location, the
panic message, and a full backtrace (captured unconditionally — no
RUST_BACKTRACE=1 needed). In addition, every normal exit appends a one-line
reason, so it is always clear why htoprs stopped:
- a quit keystroke (
reason: quit key: 'q' (ch=113)), - a terminating signal, including a controlling-terminal / SSH hangup
(
reason: signal 1 (Hangup: 1)) or a fatal fault (reason: fatal signal 11 (Segmentation fault: 11)), - stdin reaching EOF / a detached tty (
reason: stdin EOF …), - or the
-niteration limit being reached.
Entries are appended, so the history survives across runs. The path honors
$XDG_CACHE_HOME ($XDG_CACHE_HOME/htoprs/crash.log) and can be overridden
verbatim with $CRASH_LOG=/path/to/log. On a crash the restored terminal also
prints htoprs: crash logged to <path>.