// LSOFRS — LIST SYSTEM OPEN FILES IN RUST

lsofrs v4.8.5 · Zero-copy FFI · Rayon-parallel gathering · macOS + Linux + FreeBSD · 7-tab TUI · 31 cyberpunk themes · 5–21× faster than lsof / lsofng

23,441 Rust src · 8,560 test src · 1,662 #[test] functions · 13 direct dependencies

Report GitHub Issues
// Color scheme

>_LSOFRS — THE FASTEST LSOF EVER WRITTEN

Maps the invisible topology between processes and the files they hold open. Regular files, directories, sockets, pipes, devices, kqueues — anything the kernel touches. Rust rewrite of lsofng with zero-copy repr(C) FFI to Darwin / Linux / FreeBSD kernel headers, rayon work-stealing across every CPU for per-PID FD enumeration, and a single streaming pass from gather → filter → print. 14.2 ms wall-clock vs lsof's 169.8 ms on a ~470-process / ~5000-FD macOS box (hyperfine, 10 runs, 3 warmup).

Quickstart

Install from crates.io or source. The same binary is published twice — lsofrs (long) and lsf (short, quicker to type):

# install
cargo install lsofrs

# from source
git clone https://github.com/MenkeTechnologies/lsofrs
cd lsofrs && cargo build --release
sudo cp target/release/lsf /usr/local/sbin/

# everyday queries
lsf                           # list all open files
lsf -p 1234                   # files for PID 1234
lsf -c Chrome                 # files for Chrome processes
lsf -u root                   # files for root user
lsf -i                        # network connections only
lsf -i :8080                  # who's listening on port 8080
lsf /path/to/file             # who has this file open
lsf -t -c nginx               # just PIDs (for scripting)

# pipelines
lsf --json | jq '.[] | select(.fd == "cwd")'
lsf --csv  > snapshot.csv
lsf -F pcfn                   # field output (p=pid, c=cmd, f=fd, n=name)

Full install + usage live in the README. Man page: man lsofrs (310-line roff under lsofrs.1). Zsh completion ships under completions/_lsofrs.

Why lsofrs — Wall-Clock Comparison

Benchmarked on macOS with hyperfine (10 runs, 3 warmup, ~470 processes / ~5000 open files). Numbers are reproducible from README §PERFORMANCE.

Workload lsofrs (Rust) lsof 4.91 (C) lsofng (C) Speedup
All open files (default)14.2 ms169.8 ms173.0 ms12×
Network only (-i TCP)7.2 ms91.7 ms88.1 ms12–13×
Terse PIDs (-t)6.9 ms101.4 ms142.9 ms15–21×
Structured (-J JSON / -F field)29.3 ms156.1 ms136.7 ms

Feature parity vs incumbents

Feature lsofrs lsof lsofng fuser ss
Built-in JSON (--json / -J)
Built-in CSV (--csv, RFC 4180)
Field output (-F)
Full-screen tabbed TUI (--tui)7 tabs
Top-N FD dashboard (--top)
Stale-FD finder (--stale)
Listening-ports view (--ports)
Process tree (--tree)
Pipe / unix-socket IPC map (--pipe-chain)
Net-by-remote-host map (--net-map)
File-open/close watch (--watch)
FD-leak detector (--leak-detect)
Single-process follow (--follow)
Delta highlighting (--delta)
Aggregate summary + bar charts (--summary)
Themeable output (31 palettes)
Parallel per-PID gather (rayon)
Cross-platform (Darwin + Linux + FreeBSD)Linux/BSDLinux

Overview

  • CLI front-end — clap derive parser in src/cli.rs (1,341 lines, 38 #[arg(...)] attributes). main.rs dispatches to the active mode based on which flag is set.
  • Platform gathersrc/darwin.rs (1,253 lines, libproc FFI), src/linux.rs (820 lines, /proc reader), src/freebsd.rs (835 lines, sysctl + procfs). All three return the same Vec<Process> from types.rs; each per-PID FD scan is parallelized with rayon's work-stealing pool.
  • Selectionsrc/filter.rs (2,812 lines) implements OR-mode (default) and AND-mode (-a) filter composition over PID / PGID / user / command / FD / network-spec / path. Supports comma-lists, ranges (0-10), exclusion (^root), and regex (/nginx|apache/).
  • Outputsrc/output.rs (columnar + field), src/json.rs (serde), src/csv_out.rs (RFC 4180). TTY detection auto-switches between cyberpunk headers (H4XOR / CL4SS / T4RGET) and plain headers for pipelines.
  • Live modes--tui (7-tab dashboard, 5,312 lines), --top (1,008 lines), --summary -r (904 lines), --monitor (288 lines), --watch FILE, --follow PID, --leak-detect. All share src/tui_app.rs's TuiMode trait for common keybindings, alternate-screen entry/exit, and atomic frame rendering.
  • Themessrc/theme.rs (1,005 lines) defines 31 named palettes via the ThemeName enum. Custom 6-color themes persist to ~/.lsofrs.conf through src/config.rs (TOML).

Modes — one card per entry point

--tui

Unified 7-tab dashboard: TOP / SUMMARY / PORTS / TREE / NET-MAP / PIPES / STALE. Click tabs, mouse-hover for tooltips, right-click for verbose detail (PID, FD breakdown, kill / copy hints). Theme chooser (c) + editor (C).

--top [N]

Live auto-refreshing top-N processes sorted by FD count. FD-type distribution bar, delta tracking, per-process breakdown. s cycles sort column, +/- grows/shrinks N.

--summary [-r N]

Aggregate FD breakdown with bar charts, top processes, per-user totals. With -r N becomes a live TUI; --json emits a structured report; -i restricts to network FDs.

--ports

Quick "what's listening where" summary — like ss -tlnp but cross-platform (macOS + Linux + FreeBSD). --json and per-user filtering supported.

--tree

Hierarchical process tree with FD counts, per-PID type breakdown ([REG:12 IPv4:3 PIPE:2]), network connection counts, and notable open files inline.

--net-map

Network connections grouped by remote host — fastest way to see which servers your box talks to and how many sockets each peer holds.

--pipe-chain

Trace pipe and unix-socket pairs between processes — visualize the IPC topology that ps and top hide.

--stale

Find FDs pointing to deleted files. Common source of disk-space leaks, zombie handles, and security issues (process held an unlinked secret file).

--watch FILE

Print timestamped +OPEN / -CLOSE events for a single path. Lightweight inotifywait / fs_usage equivalent that runs on macOS, Linux, and FreeBSD.

--follow PID

Watch one process's FDs in real time. New opens highlight +NEW (green), closes -DEL (red).

--leak-detect[=I,N]

Sample per-process FD counts every I seconds; flag any PID whose count rises N consecutive intervals in a row. Circular buffer per PID in src/leak.rs.

--delta -r N

Color-code changes between repeat iterations. New FDs in green, removed in red — useful inside watch-style loops without the noise of full re-prints.

--monitor / -W

Classic full-screen alternate-buffer monitor like top(1). s sort / r reverse / f filter / p pause / ? help.

--csv

Pure RFC-4180 CSV dump for pipelines and spreadsheets. Composes with every selector — lsf --csv -i TCP -u root > root_tcp.csv.

--json / -J

Serde-backed JSON array. Composes with every selector. Each element carries the full OpenFile shape from src/types.rs (PID, command, FD, type, device, size/offset, node, name, plus parsed socket / protocol fields when present).

-F <chars>

Per-record field output for scripting (compat with the original lsof). p=pid, c=command, f=fd, n=name, t=type, etc.

Selection grammar

Every selector is independent and composable. By default selectors OR together; -a switches the entire predicate to AND mode. Negation via ^, ranges via N-M, regex via /…/.

lsf -p 1234,5678               # PIDs 1234 OR 5678
lsf -u root,wizard             # users root OR wizard
lsf -p ^1234                   # everything except PID 1234
lsf -u ^root                   # everything except root
lsf -d 0-10                    # FDs 0 through 10
lsf -c '/nginx|apache/'        # command name matches regex

lsf -a -p 1234 -i              # AND: PID 1234 AND has network FD
lsf -a -u nginx -i TCP:443     # AND: nginx user AND TCP/443

lsf -i                         # any network FD
lsf -i 4                       # IPv4 only
lsf -i 6                       # IPv6 only
lsf -i TCP                     # TCP only
lsf -i :443                    # any protocol on port 443
lsf -i TCP:443                 # TCP on port 443

Cyberpunk theming

When stdout is a TTY, column headers swap from utilitarian (COMMAND / PID / USER) to cyberpunk (PROCESS / PRC / H4XOR) and ANSI coloring activates. Piped output is always plain — safe for scripts. 31 named palettes ship in src/theme.rs:

Palette familyThemes
Neon / synthNeonSprawl (default), NeonNoir, Synthwave, GlitchPop, HoloShift
Matrix / phosphorMatrix, AcidRain, BioHazard, ToxicWaste
Blade Runner / noirBladeRunner, NightCity, Megacorp, Zaibatsu, VoidWalker, Darkwave, DarkSignal
Frost / iceIceBreaker, CyberFrost, GhostWire, ChromeHeart, SteelNerve, QuantumFlux
Solar / emberSolarFlare, PlasmaCore, LaserGrid, RedSector, SakuraDen
Data / minimalClassic, DeepNet, DataStream, Overlock

Inside any TUI mode press c for the theme chooser (live preview), C for the 6-color editor. Selections persist to ~/.lsofrs.conf via src/config.rs.

Interactive controls (shared across TUI modes)

KeyAction
19Set refresh interval (seconds)
< / >Fine-adjust refresh interval (±1s)
pPause / resume data collection
? / hToggle help overlay
c / CTheme chooser / editor
TToggle hover tooltips (right-click still works)
xToggle border
tToggle compact / expanded view
oFreeze / unfreeze sort order
/ / 0Filter popup (regex) / clear filter
j / k / / Navigate rows
FPin / unpin selected row
yCopy selected row to clipboard
eExport current tab to file
q / Esc / Ctrl-CQuit

--tui adds tab navigation (Tab / BackTab / 17 / click). --top adds s sort, r reverse, +/- grow/shrink, b bar toggle, d delta toggle.

Architecture in one screen

src/
├── main.rs        # 282  CLI entry, mode dispatch, repeat/leak loops
├── cli.rs         # 1,341 clap derive args (38 #[arg], 20 short flags)
├── types.rs       # 867  Process, OpenFile, SocketInfo, NetSpec, …
├── darwin.rs      # 1,253 macOS libproc FFI (zero-copy repr(C))
├── linux.rs       # 820  /proc reader (parallel per-PID via rayon)
├── freebsd.rs     # 835  sysctl + procfs reader
├── filter.rs      # 2,812 selection & filter composition (AND/OR, regex)
├── strutil.rs     # 76   safe UTF-8 truncation for fixed-width display
├── output.rs      # 772  columnar & field formatting, ANSI theming
├── json.rs        # serde JSON
├── csv_out.rs     # RFC 4180 CSV
├── monitor.rs     # 288  classic full-screen alternate-buffer monitor
├── follow.rs      # 369  single-PID FD tracker (status transitions)
├── leak.rs        # 339  circular-buffer FD-count leak detector
├── delta.rs       # iteration-diff engine for change highlighting
├── summary.rs     # 904  aggregate stats + bars (+ live TUI mode)
├── tree.rs        # 556  process tree w/ FD inheritance
├── tui_app.rs     # 564  shared TuiMode trait (ratatui)
├── tui_tabs.rs    # 5,312 unified 7-tab TUI (mouse, tooltips)
├── theme.rs       # 1,005 31 themes + custom palette support
├── config.rs      # TOML config persistence (~/.lsofrs.conf)
├── top.rs         # 1,008 live top-N FD dashboard
├── watch.rs       # 369  file-open/close watch
├── stale.rs       # 313  deleted-file FD finder
├── ports.rs       # 450  listening-ports summary
├── pipe_chain.rs  # 358  pipe / unix-socket IPC topology
└── net_map.rs     # 608  connections grouped by remote host
lsofrs.1           # 310-line roff man page
completions/_lsofrs # 101-line zsh completion (#compdef lsofrs)

Platform support matrix

OSModuleMechanismStatus
macOS / Darwinsrc/darwin.rs (1,253 lines)libproc FFI via zero-copy #[repr(C)] structs matching kernel headers; proc_listpids + proc_pidinfo + proc_pidfdinfo.Primary platform
Linuxsrc/linux.rs (820 lines)/proc/<pid>/{stat,status,fd,fdinfo} reader; parallelized per-PID with rayon.Supported
FreeBSDsrc/freebsd.rs (835 lines)sysctl(KERN_PROC_FILEDESC) + /proc when mounted; FreeBSD-specific socket / kqueue decoding.Supported

Each platform module is gated by #[cfg(target_os = "…")] in src/lib.rs. The shared Process / OpenFile / SocketInfo shapes live in src/types.rs (867 lines, 11 public structs / enums) — every gather backend returns the same data, so the entire stack above filter.rs is platform-agnostic.

Why it's fast

  • Zero-copy FFI. Darwin gather uses raw #[repr(C)] structs sized to kernel headers — no intermediate parse step, no allocator pressure per-FD.
  • Rayon work-stealing per-PID. Each PID's FD scan runs on its own worker; CPU count = parallel degree.
  • Username caching. v4.8.0 added a OnceCell-backed getpwuid cache — previously the hot loop called the syscall once per record.
  • Precomputed filter paths. The filter predicate canonicalizes its lookups once before the gather pass, not per-FD.
  • Streaming output. No intermediate Vec<Record> for the common path — gather → filter → format → write goes through the same iterator chain.
  • serde + clap derive. No hand-rolled JSON escaping, no hand-rolled getopt loop.

Net result: 14.2 ms wall-clock for the default lsf invocation vs 169.8 ms for lsof 4.91 on the same machine (hyperfine, ~470 procs, ~5000 FDs).

JSON shape

--json emits an array of records. Each record is the public OpenFile serialization from src/types.rs:

[
  {
    "pid": 1234,
    "ppid": 1,
    "pgid": 1234,
    "uid": 0,
    "user": "root",
    "command": "nginx",
    "fd": "10u",
    "type": "IPv4",
    "device": "0x0",
    "size_off": null,
    "node": "TCP",
    "name": "*:8080 (LISTEN)",
    "socket": {
      "protocol": "TCP",
      "local_addr": "0.0.0.0",
      "local_port": 8080,
      "remote_addr": null,
      "remote_port": null,
      "state": "LISTEN"
    }
  }
]

Hard-coded JSON contract tests live in tests/json_shape.rs, tests/json_wrappers.rs, and tests/json_and_csv_contracts.rs (233 #[test] functions in the latter alone) — refactors can't silently break downstream consumers.

Where to go next

  • Engineering report — per-subsystem LOC, top-20 files, dependency table, test surface.
  • README — full install, every flag, full benchmark table.
  • docs.rs/lsofrs — rustdoc for the library crate (pub mod cli; pub mod types; pub mod filter; …).
  • lsofrs.1 — 310-line roff man page (also man lsofrs after install).