What we're building: a process monitor TUI
Step 1 of 8, and it runs nothing. This is the map: what rlog does when the track is finished, and
the seven steps between here and there. Read it once so every later article has a place to land. Each
one starts from the state the previous step left behind and adds a piece to it.
A process monitor is the right first terminal UI. It has every problem a TUI exists to solve: more
information than fits on one screen, data that changes while you’re looking at it, and a reason to
use the keyboard instead of typing commands. btop and htop are the familiar examples, and
lazygit is the same idea applied to git. By the end of the track, rlog is a program in that
family — smaller, written from scratch, and small enough to read in an evening.
The finished monitor
Section titled “The finished monitor”rlog watches two things at once: a log file it follows, and the process table of the machine it
runs on. The finished screen looks like this:
┌ rlog · 214 processes · 2.4 GB resident ───────────────────────────────┐│ / filter… │├─ processes ─────────────────┬─ app.log ───────────────────────────────┤│ PID CPU% MEM% NAME │ 12:01:04 INFO worker finished batch 7 ││ ▸1042 8.1 2.4 rlog │ 12:01:05 WARN retrying upload ││ 988 6.4 1.1 nvim │ 12:01:06 ERROR connection reset ││ 812 3.0 0.9 node │ 12:01:06 INFO worker finished batch 8 │├─ cpu history ───────────────┴─────────────────────────────────────────┤│ ▁▂▄▆▇▅▃▂▄▆▇▅▃▂▁▃▅▇▆▄▂▁▃▅▆▇▅▃▂▁ │└─ q quit · / filter · s sort · r reverse · space pause · ? help ───────┘A sketch, not a screenshot: the numbers are made up, the panes are what the track builds. The top
line carries the machine summary, so the header answers “is this box busy?” before anything is
selected. Under it sits the filter line — press / and typing narrows the process table and the log
pane at once, with fuzzy matching, so ngx finds nginx.
The process table lists pid, CPU, memory and name. Arrow keys or j/k move the selection, s
cycles which column sorts, r flips the direction. CPU is the default sort because it’s the column
that changes while you watch.
The log pane follows the file given on the command line — rlog ./app.log — remembering its offset
and appending lines as they’re written. PgUp/PgDn and the mouse wheel scroll back through
history, and space freezes the tail so a burst can be read; press it again and the pane catches up.
The bottom pane is the CPU history of the selected process: a rolling window of samples drawn as a
sparkline. Selecting a different row redraws it from that process’s own history. ? overlays the
key list, and q quits, putting the terminal back exactly as it was.
The one idea: a loop, not a program
Section titled “The one idea: a loop, not a program”Everything above falls out of one shape:
loop { if let Some(event) = next_event(timeout = one tick) { state.apply(event) } state.refresh_data_if_due() if state.dirty { terminal.draw(render) }}Three kinds of thing arrive at that loop, and most of the track’s steps are about them:
- Input. Keys, mouse movements and resize events, read through crossterm. Ratatui doesn’t read the terminal itself; the program does, and hands the results to its own state.
- New log data. The file grows. The program remembers where it stopped reading and picks up from there, and notices if the file is truncated underneath it.
- A new process snapshot. The process table is read on its own, slower schedule — every second rather than every frame — because reading it is far more expensive than drawing a row.
Two mechanisms make it feel like a real application rather than a demo:
- The alternate screen. On start, crossterm switches the terminal to a second screen buffer and puts it in raw mode: keys arrive one at a time, and nothing is echoed. On exit the original screen comes back, so the shell’s scrollback survives the program.
- Diff rendering. Ratatui keeps two buffers, the frame being built and the one currently on
screen, and
drawwrites only the cells that changed. That is why a ratatui program doesn’t flicker when it redraws — provided nothing clears the screen by hand. Step 5 is where that claim gets tested.
The state itself is plain Rust — a struct of vectors and counters — and rendering is a pure function of it. That separation is what makes the sortable table and the history buffer testable without a terminal, which is where this track’s few tests live.
The three parts
Section titled “The three parts”The frame, steps 2–4. Step 2 creates the project and draws one bordered box: crossterm into raw
mode and the alternate screen, ratatui’s render loop, and a frame that appears at all. Step 3 wires
up the event loop — keys, mouse, and a resize that reflows the layout. Step 4 turns the single box
into panes: layout constraints, the Table and List widgets, and scrollback in the log pane.
The data, steps 5–6. Step 5 follows the log file, introduces the tick, and adds the dirty flag
so the screen is redrawn only when something changed. Step 6 reads the process table with sysinfo
and builds the interactive part: a selected row, sorting by column, and the fuzzy filter (built on
tui-input and nucleo). The pure logic — sort order, selection movement, filter matching — gets
unit tests here.
History and release, steps 7–8. Step 7 keeps a ring buffer of CPU samples per process and draws
the sparkline. Step 8 is the release article: what a terminal program needs from its environment,
what TERM and colour support mean for the drawing code, and how the binary ships.
The route
Section titled “The route”| # | Article | What lands |
|---|---|---|
| 2 | Drawing your first frame with Ratatui | the alternate screen and the render loop |
| 3 | Reading terminal events | keys, mouse, and resize |
| 4 | Laying out panels and tables | constraints, and a responsive layout |
| 5 | Refreshing live data without flicker | following the file, and redrawing only on change |
| 6 | A sortable process list | the process table, selection, sorting, and the filter |
| 7 | Charts and sparklines | CPU history over time |
| 8 | Releasing a cross-platform TUI | terminal support and binary distribution |
What this monitor is not
Section titled “What this monitor is not”- Not a system monitor replacement. No per-core breakdown, no network or disk panes, no process tree. The process table is a flat list, because a flat list is what the steps build well.
- Not a log aggregator. One file, followed live. No searching across files, and no parsing beyond splitting the timestamp off the front of each line.
- Not async. One thread, one loop. A blocking event read with a timeout is all the concurrency this needs; a runtime would complicate every later step for no visible gain.
- Not Windows-verified. crossterm and
sysinfoboth build on Windows, but every command in this track was run on macOS, and step 8 covers what changes on other terminals and how a release is cut.
What you need first
Section titled “What you need first”- Rust, installed and current. Every step is written and verified against rustc 1.99.0 with edition 2024, the compiler and edition the rest of this site uses. rustup.rs installs it or brings an old install up to date.
- Ownership basics. The state owns its strings and the render borrows them back. The first chapters of Ownership and borrowing are enough.
Resultand?. A file that vanishes mid-tail returns an error, and the program keeps running. Error handling covers the pattern the track uses.- Hash maps and iterators, casually. The log pane is a ring buffer plus one iterator chain. Collections and Closures and iterators have what’s needed.
No TUI experience, and no async. A terminal with alternate-screen support is assumed — every modern terminal has it. The screenshots in this track are taken on a pseudo-terminal, so they look like what a reader sees rather than like one particular terminal emulator.
Where the code lives
Section titled “Where the code lives”Step 2 starts the project: cargo new rlog in tui-ratatui/ at the root of
the examples repository, tagged tui-ch02. Every step
after that commits its end state and tags it — tui-ch03, tui-ch04, and so on — so
git checkout tui-ch06 is a process monitor that runs. This article commits nothing; the first tag
lands with article 2.
Step 2 builds the first frame: crossterm into raw mode, the alternate screen, and one bordered box drawn by ratatui. From that article on, every step ends with something running. The track overview keeps the full list current as articles land.