// LSOFRS — ENGINEERING REPORT

Rust rewrite of lsof · zero-copy repr(C) FFI · rayon work-stealing per-PID · cross-platform (Darwin + Linux + FreeBSD) · ratatui TUI with 7 tabs · 31 themes

>_EXECUTIVE SUMMARY

lsofrs is a single-binary Rust implementation of lsof. Three platform backends (src/darwin.rs via libproc FFI, src/linux.rs via /proc, src/freebsd.rs via sysctl + procfs) feed a shared Vec<Process> through a composable filter pipeline (src/filter.rs) into one of fourteen output / live-monitor modes. Per-PID FD enumeration runs on rayon's work-stealing pool; every TTY mode shares a TuiMode trait (src/tui_app.rs) for common keybindings, alternate-screen entry/exit, and atomic frame rendering. 23,441 production Rust lines + 8,560 test lines + 1,662 #[test] functions. hyperfine wall-clock: 14.2 ms for the default invocation vs 169.8 ms for lsof 4.91 (~12× speedup), 21× on terse PID output.

23,441
Production Rust Lines
8,560
Test Rust Lines
32,001
Total Rust Lines
1,662
#[test] Functions
28
Production Modules
10
Test Modules
38
CLI Args (#[arg])
31
Themes
7
TUI Tabs
13
Direct Dependencies
3
Platforms (Darwin/Linux/FreeBSD)
233
Git Commits

Source Distribution — 32,001 lines

23,057 production / 8,560 tests · 74.2% production

Production: 28 files in src/. Tests: 10 integration modules in tests/ + inline #[cfg(test)] blocks in 20 of the production files. Total #[test] count is taken from grep -rh '#[test]' tests/*.rs src/*.rs | wc -l.


~BENCHMARK POSITION

hyperfine wall-clock (10 runs, 3 warmup, ~470 processes / ~5000 open files on macOS). Reproducible from README § PERFORMANCE. The rayon-parallel per-PID enumeration + zero-copy FFI + OnceCell-cached getpwuid together push the steady-state cost below 15 ms.

Workload lsofrs (Rust) lsof 4.91 (C) lsofng (C) Speedup vs lsof
All open files (default) 14.2 ms 169.8 ms 173.0 ms 12×
Network only (-i TCP) 7.2 ms 91.7 ms 88.1 ms 13×
Terse PIDs (-t) 6.9 ms 101.4 ms 142.9 ms 15×
Structured JSON (-J) 29.3 ms 156.1 ms (-F pcfn) 136.7 ms

Where the speedup comes from

Per-PID FD enumeration runs on rayon's work-stealing pool — CPU count = parallel degree. Darwin uses raw #[repr(C)] structs sized to libproc's kernel headers, so the gather pass never allocates per-FD. v4.8.3 added a OnceCell-cached getpwuid path, precomputed canonical filter paths, and removed every per-record allocation from the hot loop (see commit 1464becf31).

Why feature set still beats both incumbents

Modes that ship in lsofrs but neither lsof nor lsofng: unified 7-tab TUI, top-N FD dashboard, summary bar charts, stale-FD finder, listening-ports view, process tree with FD-type breakdown, pipe / unix-socket IPC map, net-by-remote-host view, file-open/close watch, single-PID follow, FD-leak detector, delta highlighting, CSV export, 31 themes with editor.

Single-binary footprint

One static lsofrs / lsf binary — clap derive parser, serde JSON, RFC 4180 CSV, ratatui TUI, crossterm terminal control, nix POSIX bindings, libc FFI, rayon work-stealing, regex selector, chrono timestamps, TOML config — all linked in. 13 direct dependencies, all foundational crates likely to still build cleanly in 2035 (clap, serde, libc, nix, regex, rayon, ratatui, crossterm, chrono, toml, dirs, users, serde_json).

Cross-platform parity

All three platform backends return the same Process + OpenFile + SocketInfo shapes from src/types.rs. Everything above filter.rs is platform-agnostic, so adding a new TUI tab or output format doesn't fan out across three implementations — it ships once.


#SUBSYSTEM BREAKDOWN

Source partitioned by role. TUI & theming dominate at 31.3% of production Rust — tui_tabs.rs alone is 5,308 lines for the unified 7-tab dashboard (mouse, tooltips, theme chooser + editor). Selection / filter is the second-biggest subsystem (2,659 lines) because every selector composes with every other under OR / AND mode with regex, range, and negation grammar.

SubsystemKey FilesLines% of prodShareDescription
TUI & Themingtui_tabs, tui_app, theme, config7,22131.3%
Unified 7-tab dashboard (--tui) with mouse + tooltips + theme chooser/editor (tui_tabs.rs, 5,308); shared TuiMode trait + ratatui framework (tui_app.rs); 31 named palettes via ThemeName enum (theme.rs); TOML config persistence at ~/.lsofrs.conf (config.rs)
Selection / Filterfilter2,81212.0%
PID / PGID / user / command / FD / network-spec / path selectors, OR-mode (default) and AND-mode (-a) composition, comma-lists, ranges (0-10), exclusion (^root), regex (/…/), precomputed canonical paths
Platform Gatherdarwin, linux, freebsd2,90812.6%
macOS libproc FFI with zero-copy #[repr(C)] structs (darwin.rs, 1,253); /proc reader with parallel per-PID rayon (linux.rs, 820); sysctl + procfs decoder including kqueue (freebsd.rs, 835)
Live Modestop, summary, monitor, follow, watch, leak, delta3,78716.4%
Live top-N FD dashboard (top.rs, 1,008); aggregate summary + bars in static and live form (summary.rs, 904); classic full-screen monitor (monitor.rs); single-PID follow (follow.rs); file-open/close watch (watch.rs); circular-buffer FD-leak detector (leak.rs); iteration-diff engine for change highlighting (delta.rs)
Single-Shot Viewstree, ports, stale, pipe_chain, net_map2,2859.9%
Process tree with FD inheritance + per-PID type breakdown (tree.rs); listening-ports summary like ss -tlnp (ports.rs); deleted-file FD finder (stale.rs); pipe / unix-socket IPC map (pipe_chain.rs); connections grouped by remote host (net_map.rs)
CLI & Dispatchcli, main, lib, strutil1,7227.5%
clap derive parser with 38 #[arg] attributes and custom help display (cli.rs, 1,341); mode-dispatch entry point including repeat / leak-detect loops (main.rs, 282); library facade gated by #[cfg(target_os)] (lib.rs); safe UTF-8 truncation for fixed-width column display, no mid-codepoint slices (strutil.rs)
Output / Serializationoutput, json, csv_out1,6087.0%
Columnar + field (-F) formatting with TTY-detected ANSI theming (output.rs, 770); serde-backed JSON array (json.rs, 441); RFC 4180-compliant CSV with proper quoting (csv_out.rs, 397)
Type Systemtypes8673.8%
Shared cross-platform data shapes: Process, OpenFile, SocketInfo, NetSpec, FdType, theme enums — 11 public struct / enum definitions, all #[derive(Serialize, Deserialize)] for the JSON contract
Teststests/*.rs + #[cfg(test)] inline8,56025.8%
1,662 #[test] functions across 10 integration modules (tests/) + 20 inline #[cfg(test)] blocks. Covers JSON / CSV contracts (233 tests in json_and_csv_contracts.rs), CLI dispatch (177 in integration.rs, 86 in cli_combinations.rs, 63 in dispatch_contracts.rs), library smoke / filter semantics / color output / binary aliasing
TOTAL (incl. tests)32,001100%

$TOP 20 FILES BY SIZE

The 20 largest files account for 84.2% of the codebase (Rust under src/ + tests/). tui_tabs.rs is by far the largest single file — the entire 7-tab dashboard, mouse handling, tooltips, and theme picker live there. The four largest test modules (json_and_csv_contracts, integration, cli_combinations, dispatch_contracts) cover 6,602 lines — every flag combination, every JSON shape, every CSV escape rule.

FileLinesRole
src/tui_tabs.rs5,308Unified 7-tab TUI dashboard (TOP / SUMMARY / PORTS / TREE / NET-MAP / PIPES / STALE), mouse handling, hover & right-click tooltips, theme chooser + 6-color editor, bottom-bar segments with verbose tooltips, atomic frame rendering
src/filter.rs2,812Selection & filter composition: PID / PGID / user / command / FD / network-spec / path selectors with comma-lists, ranges, exclusion, regex; OR (default) and AND (-a) modes; precomputed canonical filter paths (v4.8.3 perf path)
tests/json_and_csv_contracts.rs2,581233 #[test] functions pinning every JSON field, every CSV escape rule, every per-flag output shape — refactors can’t silently break downstream consumers
tests/integration.rs2,090177 end-to-end tests: full binary invocation, every flag, every output mode
src/cli.rs1,343clap derive args (38 #[arg] attributes, 20 short flags), custom help display, mode-flag groupings, version printing
src/darwin.rs1,253macOS libproc FFI: proc_listpids, proc_pidinfo, proc_pidfdinfo, proc_pidpath; zero-copy #[repr(C)] structs sized to kernel headers; per-PID FD scan parallelized with rayon
tests/cli_combinations.rs1,14286 tests covering every selector / mode combinator (-a -p -u -i AND mode, regex command match, range FDs, exclusion)
src/top.rs1,009Live top-N FD dashboard: sort cycling (FDs → PID → USER → REG → SOCK → PIPE → OTHER → DELTA → CMD), distribution bar column, delta tracking, +/- to grow / shrink N
src/theme.rs1,00531 named cyberpunk palettes (NeonSprawl, BladeRunner, Matrix, SolarFlare, …) via the ThemeName enum with display name + 6-color resolver; custom-palette support persisted through config.rs
src/summary.rs905Aggregate FD breakdown with bar charts, top-N processes, per-user totals; doubles as a live TUI mode under --summary -r N via the shared TuiMode trait
src/types.rs972Cross-platform shared shapes: Process, OpenFile, SocketInfo, NetSpec, FdType, etc. — 11 public structs / enums, all serde-serializable for the JSON contract
src/freebsd.rs835FreeBSD gather: sysctl(KERN_PROC_FILEDESC) + /proc when mounted, FreeBSD-specific socket / kqueue / shm decoding
src/linux.rs820Linux gather: /proc/<pid>/{stat,status,fd,fdinfo} reader, /proc/net/{tcp,tcp6,udp,udp6,unix} parser for socket decoding; parallelized per-PID with rayon
tests/dispatch_contracts.rs78963 tests pinning the mode-dispatch matrix in main.rs: which mode wins when multiple flags are passed, exit-code contracts, alternate-screen lifecycle
src/output.rs770Columnar + field (-F) formatting, TTY-detected ANSI theming, cyberpunk header swap (PROCESS/PRC/H4XOR on TTY, COMMAND/PID/USER when piped), Unicode-safe column widths via strutil.rs
src/net_map.rs608Network connections grouped by remote host: aggregates each peer’s open-socket count, supports JSON output and per-user filtering
src/delta.rs577Iteration-diff engine for change highlighting: stable record IDs, additions in green, removals in red, used by --delta -r N and all live modes
src/tui_app.rs564Shared TuiMode trait + ratatui scaffold: alternate-screen entry/exit, common keybindings (1-9, p pause, ? help, c/C theme, x border, / filter, y copy, e export, q quit), atomic frame rendering
src/tree.rs556Hierarchical process tree with FD inheritance, per-PID type breakdown ([REG:12 IPv4:3 PIPE:2]), notable-file listing inline; JSON tree with nested children
src/ports.rs450Listening-ports summary like ss -tlnp but cross-platform (macOS + Linux + FreeBSD); supports JSON output and per-user filtering
TOP 20 SUBTOTAL26,06981.9% of 32,001-line Rust slice

@EXECUTION PIPELINE

One invocation flows through three stages: gather (platform-specific FFI, parallel per-PID), filter (composable selectors, AND/OR mode), output (one of: columnar TTY, columnar plain, JSON, CSV, field, or live TUI). The shared Process / OpenFile shape from src/types.rs means every backend feeds every output mode without intermediate marshaling.

  argv  ──▶  cli.rs (clap derive, 38 args)
                  │
                  ▼
         ┌────────────────┐
         │   main.rs      │  mode dispatch:
         │   (282 lines)  │   tui / top / summary -r / monitor /
         │                │   watch / follow / leak-detect /
         │                │   tree / ports / stale / pipe-chain /
         │                │   net-map / csv / json / field / list
         └────────┬───────┘
                  │
       ┌──────────┴──────────┐
       │                     │
       ▼                     ▼
 #[cfg(target_os = …)]  filter.rs  (2,659 lines)
 ┌───────────┐           - PID / PGID / USER / CMD
 │ darwin.rs │           - FD range / exclusion
 │ (1,253)   │           - network spec (4|6|TCP|UDP|:port)
 │ libproc   │           - path canonicalization
 │ FFI       │           - AND mode (-a) / OR mode (default)
 │ repr(C)   │           - regex via /…/
 └─────┬─────┘
       │
 ┌───────────┐
 │ linux.rs  │
 │ (820)     │
 │ /proc fs  │     ┌──────────────────────┐
 │ rayon     │ ──▶ │  Vec<Process>        │
 │ per-PID   │     │  (types.rs, 867 ln)  │
 └─────┬─────┘     │   Process            │
       │           │   ├─ OpenFile        │
 ┌───────────┐     │   │   ├─ FdType      │
 │freebsd.rs │     │   │   └─ SocketInfo  │
 │ (835)     │     │   └─ command/uid/…   │
 │sysctl+proc│     └──────────┬───────────┘
 └───────────┘                │
                              ▼
                ┌─────────────────────────────┐
                │   output dispatch           │
                ├─────────────────────────────┤
                │ output.rs   - columnar/TTY  │
                │ json.rs     - serde JSON    │
                │ csv_out.rs  - RFC 4180      │
                │ tui_app.rs  - TuiMode trait │
                │   └─ tui_tabs, top,         │
                │      summary, monitor,      │
                │      follow, watch, leak    │
                └─────────────────────────────┘

&TYPE SYSTEM

Eleven public struct / enum definitions in src/types.rs (867 lines) carry every fact between the gather, filter, and output layers. All of them #[derive(Serialize, Deserialize)] so the JSON contract is one derive macro — no hand-rolled escaping, no schema drift.

Process

Per-PID record: pid, ppid, pgid, uid, user, command, open_files: Vec<OpenFile>. Populated by each platform backend, consumed by every filter and output mode.

OpenFile

One row of lsof output: fd, fd_kind, fd_type (FdType enum), device, size_off, node, name, optional SocketInfo when the FD is a socket.

FdType

Enum of the file-descriptor classes lsofrs decodes: REG, DIR, CHR, BLK, FIFO, PIPE, IPv4, IPv6, unix, KQUEUE, POSIX_SHM, POSIX_SEM, plus platform-specific extras.

SocketInfo

Decoded socket facts: protocol (TCP/UDP/unix), local_addr + local_port, remote_addr + remote_port, state (LISTEN/ESTABLISHED/…). Drives --ports, --net-map, and -i filtering.

NetSpec

Parsed form of the -i argument: address-family (any/4/6), protocol (any/TCP/UDP), host, port. One parsed predicate filters against every SocketInfo.

FilterOptions

The composed selector bundle handed to filter.rs: lists of PIDs / users / commands / FDs / paths / net-specs, plus AND-vs-OR mode bit. Single owner of every selector flag.

OutputFormat

Enum chosen by cli.rs: Columnar, Field(<chars>), Json, Csv, Terse. main.rs picks the right serializer per variant.

ThemeName

31-variant enum of named cyberpunk palettes (NeonSprawl, BladeRunner, Matrix, …). ThemeName::ALL is the canonical iteration order used by the theme chooser.

LsofTheme

Resolved 6-color palette ready for ratatui: header / row / accent / good / warn / bad. Built from a ThemeName or from a custom palette loaded out of ~/.lsofrs.conf.

SortColumn / SortDir

Cycled by s / r in the top dashboard. Drives all live-mode column ordering through the shared TuiMode trait.

DeltaState

Iteration-to-iteration diff bookkeeping consumed by delta.rs. Tracks which records are NEW (green), GONE (red), or UNCHANGED so live modes can highlight churn.


%MODES MATRIX

Fifteen distinct entry points dispatched from main.rs based on which flag is set. Single-shot modes print and exit; live modes share tui_app.rs's TuiMode trait for common keybindings and atomic frame rendering.

FlagModuleLinesModeDescription
(default)output.rs770single-shotColumnar output (cyberpunk on TTY, plain when piped); the original lsof default
--tuitui_tabs.rs5,308live (TuiMode)Unified 7-tab dashboard with mouse + tooltips + theme picker + theme editor
--top [N]top.rs1,008live (TuiMode)Live top-N processes sorted by FD count with distribution bars + delta
--summary [-r N]summary.rs904bothAggregate stats + bar charts; -r N turns it into a live TuiMode
--monitor / -Wmonitor.rs288live (TuiMode)Classic full-screen alternate-buffer monitor (top(1)-style)
--watch FILEwatch.rs369live (stream)Timestamped +OPEN / -CLOSE events for a single path
--follow PIDfollow.rs192liveSingle-PID FD tracker; new opens +NEW green, closes -DEL red
--leak-detect[=I,N]leak.rs339livePer-PID FD-count sampler with circular buffer; flags monotonically increasing counts
--treetree.rs556single-shotHierarchical process tree with per-PID FD-type breakdown
--portsports.rs450single-shotListening-ports summary like ss -tlnp, cross-platform
--stalestale.rs313single-shotDeleted-file FD finder (disk-space leak / zombie-handle detector)
--pipe-chainpipe_chain.rs358single-shotPipe + unix-socket IPC topology between processes
--net-mapnet_map.rs608single-shotNetwork connections grouped by remote host
--csvcsv_out.rs397single-shotRFC 4180-compliant CSV export
--json / -Jjson.rs441single-shotSerde-backed JSON array (full OpenFile shape)
--delta -r Ndelta.rs577live (repeat)Color-coded diff between iterations (green=new, red=gone)
-F <chars>output.rssingle-shotPer-record field output (p=pid, c=cmd, f=fd, n=name, …) — lsof-compatible
-toutput.rssingle-shotTerse PIDs-only output (scripting path)

!DEPENDENCY AUDIT

Thirteen direct dependencies, all foundational crates from the durable-by-design tier (libc, nix, serde, clap, regex, rayon, chrono) with one TUI layer (ratatui + crossterm). Picked for "will this still build cleanly in 2035" — no curl-pipe-bash installers, no flaky build scripts, no nightly-only features.

CrateVersionPurpose
clap4.x (derive)CLI parsing with derive macros — backs cli.rs's 38 #[arg] attributes
serde1.x (derive)Trait-driven serialization for JSON + TOML config; one derive macro pins the JSON contract
serde_json1.xJSON serializer used by --json / -J
crossterm0.29Terminal control (alternate screen, raw mode, key/mouse events) under every live mode
libc0.2Raw FFI types for Darwin libproc structs and POSIX getpwuid
nix0.31Safe POSIX bindings — signal, term, ioctl, user, fs, net, hostname feature set
users0.11Username lookup wrapped in a OnceCell cache (v4.8.3 hot-path optimization)
regex1.xRegex matcher for command-name selectors via /…/ grammar
chrono0.4Timestamps for --watch open/close events and live-mode footers
rayon1.xWork-stealing thread pool for per-PID FD enumeration — CPU count = parallel degree
ratatui0.30TUI rendering primitives (widgets, layouts, frame) under tui_app.rs's TuiMode trait
toml1.1Config-file parser for ~/.lsofrs.conf (theme persistence + custom palettes)
dirs6.xOS-correct home-directory lookup for ~/.lsofrs.conf
TOTAL13 directAll from the foundational / durable tier — no nightly, no proc-macro-heavy frameworks, no opinionated DI containers

?TEST SURFACE

1,662 #[test] functions across 10 integration modules and 20 inline #[cfg(test)] blocks. The integration suite is contract-heavy: every JSON field, every CSV escape, every CLI flag combination is pinned so refactors can't silently shift output shape.

Test ModuleLines#[test]Focus
tests/json_and_csv_contracts.rs2,581233Every JSON field, every CSV escape, every per-flag output shape
tests/integration.rs2,090177End-to-end binary invocation, every flag, every output mode
tests/cli_combinations.rs1,14286Selector / mode combinator matrix (-a -p -u -i, regex, ranges)
tests/dispatch_contracts.rs78963Mode-dispatch precedence in main.rs, exit-code contracts, alternate-screen lifecycle
tests/library_smoke.rs39328Library-crate facade smoke tests (pub mod cli; pub mod types; pub mod filter; …)
tests/json_wrappers.rs39143JSON envelope shapes for the structured modes (summary, ports, tree, …)
tests/filters_and_paths.rs26826Path canonicalization, FD-range parsing, exclusion grammar
tests/json_shape.rs14713Top-level JSON shape pins for the default --json output
tests/color_output.rs1466TTY-vs-piped ANSI behavior — cyberpunk on TTY, plain when piped
tests/lsf_binary.rs525lsf short-form alias contract — same binary, same behavior, both bin targets in Cargo.toml
tests/ subtotal8,56068010 integration modules
+ inline #[cfg(test)] in src/94720 modules with inline test blocks (filter.rs alone has 208 inline tests)
TOTAL8,560+1,662Pinned contracts across CLI, JSON, CSV, filters, dispatch, color, library facade

*SHIPPING ARTIFACTS

Two binaries from one source

Cargo.toml declares both [[bin]] name = "lsofrs" and [[bin]] name = "lsf" at the same src/main.rs path. The short form is a quicker-to-type alias — same code, same behavior, both end up on PATH after cargo install lsofrs.

Man page

310-line roff page at lsofrs.1 — every flag documented, copy with sudo cp lsofrs.1 /usr/local/share/man/man1/ then man lsofrs.

Zsh completion

101-line #compdef lsofrs script at completions/_lsofrs; drop into any fpath entry (e.g. /usr/local/share/zsh/site-functions/) and reload via compinit.

Library crate

The lsofrs crate also exposes a library facade (pub mod cli; pub mod types; pub mod filter; pub mod output; pub mod json; pub mod csv_out; …) gated by #[cfg(target_os)] so downstream tools can reuse the gather + filter + serialize stack without shelling out. Documented on docs.rs/lsofrs.

Config persistence

~/.lsofrs.conf is a TOML file written by src/config.rs. Stores the last-used --tui tab, the active ThemeName, and any custom 6-color palettes built in the in-app editor (C).

CI

GitHub Actions workflow at .github/workflows/ci.yml — rustfmt, clippy, test on stable. Rust toolchain pinned to stable via rust-toolchain.toml with rustfmt + clippy components required.


=INVARIANTS

Constraints the codebase enforces or relies on. Listed here so future changes know which lines are load-bearing.

Single gather shape

All three platform backends must return Vec<Process> using the same Process / OpenFile / SocketInfo types from src/types.rs. Every output mode + filter consumes the same shape — no per-platform output branches.

TTY-detected coloring only

Output coloring + cyberpunk headers (H4XOR, CL4SS, T4RGET) are gated by std::io::IsTerminal. Pipelines always get plain ASCII headers and no ANSI escapes — safe for downstream tools like jq, awk, spreadsheets.

JSON contract is pinned

233 tests in tests/json_and_csv_contracts.rs + 43 in tests/json_wrappers.rs + 13 in tests/json_shape.rs = 289 JSON-shape tests. Renaming a field in OpenFile breaks the build until tests are updated — downstream consumers can't be silently broken.

No mid-codepoint slices

src/strutil.rs provides safe UTF-8 truncation for fixed-width column display. Output code never slices bytes by index — international filenames + emoji process names display cleanly without panics.

One shared TuiMode trait

Every live mode (--tui, --top, --summary -r, --monitor, …) implements the TuiMode trait in src/tui_app.rs. Adding a new keybinding once propagates everywhere; no per-mode reimplementations of q / p / ? / c.

OR mode is the default

Selectors compose with OR by default; -a switches the whole predicate to AND. This matches lsof's grammar so existing scripts port directly — pinned by tests in cli_combinations.rs.

Zero-allocation hot loop

v4.8.3 removed every per-record allocation from the gather loop: OnceCell-cached getpwuid, precomputed filter paths, no per-FD intermediate Vec. This is what bought the 12× default-mode speedup over lsof 4.91 — future PRs touching darwin.rs / linux.rs / filter.rs hot paths should re-bench with hyperfine.

Both binaries always ship together

Cargo.toml has [[bin]] name = "lsofrs" and [[bin]] name = "lsf" at the same src/main.rs path. tests/lsf_binary.rs pins this aliasing — lsf must remain a name-only alias forever; behavior divergence is a regression.


>>WHERE NEXT