In Part 2 we move past the barebones scaffolding from Part 1 and build what actually feels like a typing trainer: a proper Ratatui UI.
Instead of rendering the target text as a plain paragraph, we now render a typing view where every cell is classified as:
- ✅ Hit (correct character)
- ❌ Miss (incorrect character)
- ⬜ Empty (not typed yet)
On top of that, we center the typing area in the terminal and place the cursor exactly where the next character should go.
What We Covered
- Added a
Characterenum to represent Hit / Miss / Empty - Implemented
classify_character()to turn (target, input) into aCharacter - Built
State::build_line()andState::build_page()to generate renderable lines/pages - Switched rendering to Ratatui
Span/Lineso each character can be styled independently - Centered the typing area horizontally + vertically based on the current terminal size
- Added cursor positioning so the terminal cursor follows the typing position
Design Insights
Model First: UI Becomes Easy
The big shift in this episode is that we don’t “render strings” anymore: we render typed state.
Once we represent every position as a Character, the UI becomes a simple mapping:
Hit(c)→ normal spanMiss(c)→ styled span (and we special-case spaces so mistakes are visible)Empty(c)→ dim/secondary styled span
This is one of those patterns that scales nicely: the rendering code stays clean even as the app grows.
Build a Page From Two Strings
We also start treating the target + input as two aligned streams and building a page from them. That gives us a nice place to later add:
- wrapping/reflow
- scrolling
- multi-paragraph selection
- stats overlays
But for now: just “turn state into renderable lines.”
Implementation Highlights
1) Character enum + classification
We introduce a tiny enum that encodes the UI truth for each cell:
Then classify_character(target, input) decides which variant we get. This is the heart of the UI: it converts raw characters into something we can style and render consistently.
2) Building lines and pages
To render multi-line text, State builds:
- a Line:
Vec<Character> - a Page:
Vec<Line>
The idea is simple:
- iterate over target + input chars
- classify each position into a
Character - collect into a line
- do that for every target line → page
This makes the UI rendering phase extremely straightforward: render a Page, not raw strings.
3) Ratatui rendering with Span + Line
Instead of a single Paragraph::new(String), we now build Vec<Line> where each line contains a list of Spans (one per character), each with its own style.
That’s how we get fine-grained coloring and formatting without fighting the widget system.
We also add basic UI config for colors (ANSI indexed colors), e.g. one for misses and one for empty/untyped characters.
4) Centering the typing area
Terminal UIs look instantly better when the “main content” isn’t glued to the top-left corner.
We compute:
- the total text height
- the max line width
- a vertical and horizontal margin
…then use Ratatui Layout to carve out a centered rectangle and render the paragraph there.
5) Cursor placement
Finally, we place the cursor exactly where the next input should happen:
cursor_row= number of\nin inputcursor_col= distance from last newline
Then we offset those coordinates into the centered render area and call frame.set_cursor_position(...).
This is small, but it’s a huge step in making the app feel “real”.
What’s Next?
Now that we have a real typing UI, the next episode(s) can focus on turning the app into a functional trainer:
- updating
Stateon keystrokes (insert, backspace, ctrl+word delete) - computing and displaying stats (WPM, accuracy) using the new UI foundation
Project Code
You will find the complete source code here: typegym