README.md

September 3, 2026 · View on GitHub

██╗  ██╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗
██║  ██║╚══██╔══╝██╔═══██╗██╔══██╗██╔══██╗██╔════╝
███████║   ██║   ██║   ██║██████╔╝██████╔╝███████╗
██╔══██║   ██║   ██║   ██║██╔═══╝ ██╔══██╗╚════██║
██║  ██║   ██║   ╚██████╔╝██║     ██║  ██║███████║
╚═╝  ╚═╝   ╚═╝    ╚═════╝ ╚═╝     ╚═╝  ╚═╝╚══════╝

Rust htop status MenkeTechnologies

[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 .c files. 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 with ERROR: 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 snapshot tests/data/htop_c_fn_names.txt is committed, so cargo build and the port-purity gate work from a bare clone.

  • Port tree: src/ported/<file>.rs — one Rust module per C file. Each fn carries a /// Port of citation 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::theme holds the named color-scheme system (31 built-in 6-color palettes plus custom-theme plumbing), and extensions::overlay the themed keyboard-help overlay, theme chooser, and theme editor (rendering into a ratatui::Buffer) — both ported from iftoprs. extensions::colors makes a chosen theme recolor the live htop UI in 256-color via a base16-style ANSI palette remap consulted at the single Ncurses::to_color choke point, and extensions::prefs persists the selection to ~/.config/htoprs/prefs.json. The overlay is wired into ScreenManager_run: z opens the theme (color scheme) chooser, ~ the editor, h/?/F1 the themed help overlay (Esc closes it), g toggles the header, B toggles the border. These are all keys htop leaves free — lowercase c (tag process + children) and C (setup) belong to htop, so the overlay must not shadow them; likewise b is the bar fill-style cycler below and x is htop's file-locks screen. Toggles and the bar-style change surface a transient status toast (overlay::draw_status, ported from iftoprs). extensions::bridge materializes the live ported Process rows as the Proc model (via Object::as_process), and extensions::panels is 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:

    KeyCapability
    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); w writes the snapshot JSON
    oExport 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 (t toggles 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 (Tab cycles the key)
    :Command palette — fuzzy-search every action by name and run it (reuses the f matcher); reaches both extension and htop actions
    vCycle 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 ported fn): 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 the v sparkline is drawn on the rows. v cycles 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 the G history graph, and its height scales with the process's CPU: idle processes stay a single line, busy ones grow up to SPARK_GRAPH_H graph lines (more CPU = more rows). This makes the process panel variable-height: Panel_draw/Panel_onKey project every screen-Y, page step, and scroll clamp through per-row heights (item_height = 1 + graph_lines(cpu) for a process, 1 otherwise), so the cursor, paging, and scrolling all track whole processes. Non-process panels keep rowHeight = 1 and 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-v2 memory.max, else the process address-space RLIMIT_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"), and t enables 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): b cycles 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.json and is restored on launch. It is consulted by the ported BarMeterMode_draw fill loop (barstyle::fill_glyph, None ⇒ htop's native glyph) and wired into the keybinding table (Action_setBindings binds the keys['b'] slot htop leaves free unless HAVE_BACKTRACE_SCREEN is set) as an Htop_Action.

  • Port-purity gate (build.rs): on every cargo build / cargo test / cargo check that touches src/ported/, every free fn name is checked against the htop C-function snapshot at tests/data/htop_c_fn_names.txt. A fn whose name has no C counterpart fails the build. Rust-original helpers are rejected; genuine architectural helpers must be justified in tests/data/fake_fn_allowlist.txt. The gate cannot be bypassed by cargo test --test X.

  • C-name snapshot: regenerate with tests/data/extract_c_fn_names.sh after 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, one todo!() stub pub fn per C function (named to satisfy the gate). Stubs are placeholders, not ports; replace each with a faithful body and a /// Port of cite. Already-ported modules are never overwritten.

  • Port report: scripts/gen_port_report.py walks the C source and the Rust port and writes docs/port_report.html with per-file and overall coverage, derived from source at run time (nothing hardcoded). A todo!() / 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 functionnotes
String_catconcatenation
String_trimtrims leading/trailing space, tab, newline (only those three)
String_splitsplits on a separator; interior empties kept, trailing empty dropped
String_splitFirstsplits on first separator only
String_contains_icase-insensitive substring; |-multi-needle mode
compareRealNumbersNaN-aware ordering (NaN sorts first)
sumPositiveValuessum of strictly-positive values, NaN skipped
countDigitsdigit count in a given base, with overflow guard
countTrailingZerosmod-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 functionnotes
swapexchange two slots
partitionLomuto partition, pivot moved to right
quickSortrecursive quicksort, pivot left + (right - left) / 2
insertionSortin-place insertion sort over [left, right]
Vector_indexOflinear search, C -1 sentinel preserved

Hashtable.c — prime-table math:

C functionnotes
nextPrimesmallest OEIS prime >= n; aborts (panics) if none fits

Meter.c — value formatting:

C functionnotes
Meter_humanUnitkibibytes → human-readable string (KQ, 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 (htoprshtop, 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 -n iteration 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>.