Skip to content
Select theme

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.

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.

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 draw writes 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 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.

# 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
  • 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 sysinfo both 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.
  • 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.
  • Result and ?. 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.

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.