Building ratatop: a Rust System Monitor with ratatui Developer Maneshwar built ratatop, a Linux system monitor using the Rust ratatui library, as a learning project to explore immediate-mode terminal user interfaces. The project contrasts with the developer's previous Bubble Tea-based TUI app peektea, highlighting ratatui's library approach versus Bubble Tea's framework architecture. Hello, I'm Maneshwar. I'm building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback. For a long while now I've been trying to learn how to build TUI apps. Last month I made peektea, a keyboard-driven terminal file browser. The whole journey is written up here: the peektea series . A minimal terminal file browser built with Bubble Tea https://github.com/charmbracelet/bubbletea . Peek through your filesystem with arrow keys or vim keys , then pour each file straight into the app you've configured for it. A quick peek before you steep Click below to watch the demo on YouTube 7m , or scroll down to see gif 10s demo in gif version https://www.youtube.com/watch?v=yDNN5x9Y9Ok One-liner: curl -fsSL https://raw.githubusercontent.com/lovestaco/peektea/master/scripts/install.sh | sh Download a binary no Go required — grab the latest release for your platform from the releases page https://github.com/lovestaco/peektea/releases/latest : | Platform | File | |---|---| | Linux x86-64 | peektea linux amd64.tar.gz | | Linux arm64 | peektea linux arm64.tar.gz | | macOS x86-64 | peektea darwin amd64.tar.gz | | macOS Apple Silicon | peektea darwin arm64.tar.gz | Extract and put the peektea binary anywhere on your $PATH . Install with Go: go install github.com/lovestaco/peektea@latest Build from source: git clone https://github.com/lovestaco/peektea cd peektea make install make install puts the binary in ~/go/bin and figures out $PATH for you: ~/.local/bin is on…That one was Bubble Tea https://github.com/charmbracelet/bubbletea with Go. So I got curious about what the other side looks like. What can Rust do here? Which is how I landed on ratatui https://ratatui.rs . I'm connected to Orhun Parmaksız https://github.com/orhun on LinkedIn, I keep seeing his posts about it on my feed, and eventually curiosity won. So here we are. I built a basic Linux visual monitor to learn the library: For the next few days I'll be poking at this and writing down whatever I learn. This is post one. ratatui is a Rust library for building terminal user interfaces. It grew out of tui-rs , which went unmaintained back in 2023, and a group of folks picked it up and ran with it. Orhun is one of the maintainers. The name is a Ratatouille https://en.wikipedia.org/wiki/Ratatouille film joke, and honestly the film's whole "anyone can cook" thing turned out to be the right energy for a first project. You do not need to be a Rust wizard to get something on screen. One thing to get straight early: ratatui is a library , not a framework. Bubble Tea hands you an architecture. You fill in Model , Update , View , and it runs the show. ratatui hands you a drawing surface and says good luck. That difference is the whole post, really. Bubble Tea is built on the Elm Architecture https://guide.elm-lang.org/architecture/ . State goes in, messages come back, you return a new model, the framework re-renders. It is retained and it is opinionated. ratatui is immediate mode . There is no widget tree living in memory between frames. There is no View that the framework calls for you. Every single frame, you build the widgets from scratch, draw them, and throw them away. Coming from Bubble Tea this felt wrong for about a day. Where does the state live? Wherever you want. Who calls the render? You do. When do widgets get cleaned up? They were never alive. Then it clicked, and it felt great. No lifecycle, no diffing your own state, no "why did this component not re-render". You want something on screen, you draw it. Every frame. The catch is that "you own the loop" also means you own everything else. Quitting, resizing, timing, keyboard input, all of it is yours now. Here is genuinely the whole shape of a ratatui program: php fn main - std::io::Result< { let mut terminal = ratatui::init ; // alt screen + raw mode let result = run &mut terminal ; ratatui::restore ; // put the terminal back result } fn run terminal: &mut DefaultTerminal - std::io::Result< { loop { terminal.draw |frame| { let hello = Paragraph::new "hello from the kitchen" ; frame.render widget hello, frame.area ; } ?; if event::poll Duration::from millis 100 ? { if let Some key = event::read ?.as key press event { if key.code == KeyCode::Char 'q' { return Ok ; } } } } } ratatui::init is the modern convenience wrapper. It flips on raw mode and switches to the alternate screen buffer, the same trick vim and htop use so your shell history stays clean when you quit. In peektea that was tea.WithAltScreen . Same idea, different kitchen. ratatui::restore puts everything back. Worth wiring this into a panic hook too, because a Rust panic inside raw mode leaves your terminal in a genuinely cursed state. Input is not ratatui's job at all. That comes from crossterm https://github.com/crossterm-rs/crossterm , which ratatui re-exports as ratatui::crossterm so your versions cannot drift apart. event::poll with a timeout is what keeps the loop from blocking, so you can redraw on a timer instead of only on keypress. The core layout idea is small: you have a Rect , and you cut it into smaller Rect s. let header, body, footer = Layout::vertical Constraint::Length 1 , // exactly one row Constraint::Min 0 , // whatever is left over Constraint::Length 1 , // exactly one row .areas area ; That .areas returning a fixed-size array is lovely. The compiler checks that you destructured exactly three areas from three constraints. Get it wrong and it does not build. The constraint types cover most of what you want: Length n for exact cells Min n and Max n for flexible with a bound Percentage n and Ratio a, b for proportional Fill weight for splitting leftovers by weightThere is also .spacing n to put gaps between chunks, and .flex Flex::Center to control where slack goes when your constraints do not fill the space. I used Flex::Center to centre a box, then swapped it to Flex::Start to pin it to the top of the terminal. One enum, done. Nesting is just calling Layout again on one of the pieces you got back. That is the whole layout system. A widget in ratatui is a plain value that gets eaten when you draw it. The trait is literally: pub trait Widget { fn render self, area: Rect, buf: &mut Buffer ; } Note self , not &self . It is consumed. You are not holding onto it. Stock widgets get you surprisingly far. Block is the bordered container, and it composes nicely: js let block = Block::default .borders Borders::ALL .border type BorderType::Rounded // ╭─╮ instead of ┌─┐ .title "CPU" .title bottom Line::from "q to quit" .right aligned ; let inner = block.inner area ; // the area inside the borders block.render area, buf ; block.inner area is the one to remember. It gives you the space inside the border so your content does not get eaten by the frame. For text, the hierarchy is Span styled string inside Line a row inside Text many rows : Line::from vec Span::styled "C", Style::default .fg Color::Red , Span::styled "PU", Style::default .fg Color::White , There is also a Stylize trait that makes this much less shouty, giving you "CPU".red .bold directly on string literals. Very nice for quick work. This is where it got fun. The look I was going after uses braille https://en.wikipedia.org/wiki/Braille Patterns characters to fit more resolution into a terminal cell. Each braille glyph is a 2x4 grid of dots, so one character cell holds 8 addressable points instead of 1. No stock widget does that. So you write one. You implement Widget , you get a Buffer , and you set cells directly. impl Widget for MyGraph<' { fn render self, area: Rect, buf: &mut Buffer { let cell = &mut buf x, y ; cell.set char '⣿' ; cell.set style Style::default .fg Color::Green ; } } Buffer is a flat grid of Cell s, where each cell holds a symbol plus its style. That is the entire drawing surface. Once you realise a custom widget is just "write characters into a grid", the intimidation factor drops off a cliff. Then I spent an embarrassing amount of time on the fact that braille dot numbering is not sequential. The bits go 1, 2, 3 down the left column, 4, 5, 6 down the right, and then 7 and 8 got bolted on later as a fourth row. So the bitmask is 0x01, 0x02, 0x04, 0x40 on the left and 0x08, 0x10, 0x20, 0x80 on the right. That 0x40 sitting where 0x08 should be is a 19th century design decision reaching out to ruin your afternoon. Worth it though. The payoff is graphs that look like graphs instead of blocks. While drawing per-core graphs, I decided that any non-zero reading should light at least one dot. My reasoning: an idle core showing literally nothing looks broken. Reasonable. Also completely wrong. The aggregate CPU line is never zero. So every single column got exactly one dot, and my beautiful history graph rendered as a perfectly flat line, all the way across, forever. I had built a very expensive way to draw a dash. The fix was to delete my clever bit and let low values round down to blank. Those gaps are the entire texture. Letting values fall to nothing is what makes it look alive. Last piece, because it explains a debugging trap I fell into. ratatui keeps two buffers and only emits the cells that actually changed. That is why redrawing everything every frame is not wasteful. The trap: I tried to test a feature by piping the app's output and grepping for a string I expected on screen. Found nothing, assumed the feature was broken. It was not. Because of the diffing, a changed value never appears as a contiguous string in the output stream. It arrives as a few individual cells with cursor moves between them. Lesson: test your state and your widgets, not the escape codes. If you've been on the fence about TUIs, ratatui is a genuinely nice place to start. The API is small, the docs are good, and the immediate mode model means there is very little machinery hiding from you. Code is here if you want a look: lovestaco/ratatop A reimplementation of btop https://github.com/aristocratos/btop in Rust, built on ratatui https://ratatui.rs . The goal is a resource monitor that feels like btop: the four-box layout, braille graphs with gradient fills, mouse-driven navigation, and a themable process manager, but written as a modern Rust TUI. The CPU box works. It samples real data and renders btop's layout: Per-core braille history graphs, a gradient load bar with sub-cell resolution frequency, package temperature, battery, and load averages. Everything else, the memory, network, and process boxes, is still to come. /proc and /sys on Linux first, read directly rather than shelling out. btop's src/linux/btop collect.cpp is the reference for what to read and how often; the BSD/macOS collectors are out of scope for v1.More soon. Anyone can cook. AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free. Any feedback or contributors are welcome It's online, source-available, and ready for anyone to use. ⭐ Star it on GitHub: | 🇩🇰 Dansk https://github.com/HexmosTech/git-lrc/readme/README.da.md | 🇪🇸 Español https://github.com/HexmosTech/git-lrc/readme/README.es.md | 🇮🇷 Farsi https://github.com/HexmosTech/git-lrc/readme/README.fa.md | 🇫🇮 Suomi https://github.com/HexmosTech/git-lrc/readme/README.fi.md | 🇯🇵 日本語 https://github.com/HexmosTech/git-lrc/readme/README.ja.md | 🇳🇴 Norsk https://github.com/HexmosTech/git-lrc/readme/README.nn.md | 🇵🇹 Português https://github.com/HexmosTech/git-lrc/readme/README.pt.md | 🇷🇺 Русский https://github.com/HexmosTech/git-lrc/readme/README.ru.md | 🇦🇱 Shqip https://github.com/HexmosTech/git-lrc/readme/README.sq.md | 🇨🇳 中文 https://github.com/HexmosTech/git-lrc/readme/README.zh.md | 🇮🇳 हिन्दी https://github.com/HexmosTech/git-lrc/readme/README.hi.md | GenAI today is a race car without brakes . It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents silently break things : they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production. git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen At a glance: 10 risk categories https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · 100+ failure patterns tracked https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · every commit… AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production. git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free. Any feedback or contributors are welcome It's online, source-available, and ready for anyone to use. ⭐ Star it on GitHub: | 🇩🇰 Dansk https://github.com/HexmosTech/git-lrc/readme/README.da.md | 🇪🇸 Español https://github.com/HexmosTech/git-lrc/readme/README.es.md | 🇮🇷 Farsi https://github.com/HexmosTech/git-lrc/readme/README.fa.md | 🇫🇮 Suomi https://github.com/HexmosTech/git-lrc/readme/README.fi.md | 🇯🇵 日本語 https://github.com/HexmosTech/git-lrc/readme/README.ja.md | 🇳🇴 Norsk https://github.com/HexmosTech/git-lrc/readme/README.nn.md | 🇵🇹 Português https://github.com/HexmosTech/git-lrc/readme/README.pt.md | 🇷🇺 Русский https://github.com/HexmosTech/git-lrc/readme/README.ru.md | 🇦🇱 Shqip https://github.com/HexmosTech/git-lrc/readme/README.sq.md | 🇨🇳 中文 https://github.com/HexmosTech/git-lrc/readme/README.zh.md | 🇮🇳 हिन्दी https://github.com/HexmosTech/git-lrc/readme/README.hi.md | GenAI today is a race car without brakes . It accelerates fast -- you describe something, and large blocks of code appear instantly. But AI agents silently break things : they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior -- without telling you. You often find out in production. git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen At a glance: 10 risk categories https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · 100+ failure patterns tracked https://github.com/HexmosTech/git-lrc what-git-lrc-checks-for · every commit…