Rendering Pipeline
Note: This chapter is a machine-translated English version of the original Japanese chapter レンダリングパイプライン. Some phrasing may read unnaturally.
Overall flow
The server updates its own Grid, but the client also owns a drawing-only TerminalSink. It reapplies the received raw bytes locally and updates a separate Grid for rendering. That way, the drawing path does not depend on the server crate.
What Grid is responsible for
Grid in crates/terminal/src/grid/mod.rs is not just a 2D character array.
It is a virtual screen buffer that combines:
- cell storage
- cursor position
- terminal flags such as DECAWM and LCF
- saved main-screen state for alternate screens
- DECSTBM scroll-region state
- scrollback
- dirty-row tracking
Cell representation
Each Cell has content and style. Content is represented with CellContent, and the current implementation still uses Continuation to occupy the right-hand side of full-width graphemes.
That representation makes it easier to treat the following as “one grapheme = one drawing unit”:
- full-width characters
- emoji sequences forced by VS-16
- composed emoji containing ZWJ
VT sequence processing
VtProcessor implements vte::Perform and mainly updates:
| callback | purpose |
|---|---|
print | writing regular characters |
execute | CR / LF / BS / BEL |
csi_dispatch | cursor movement, erase, scroll region, SGR |
osc_dispatch | title changes, OSC 52, notifications, OSC 133;D |
The important point in the current implementation is that VtProcessor extracts more than visible text.
It also pulls out notifications, clipboard writes, and command completion:
notification: Option<String>clipboard_data: Option<Vec<u8>>command_finished: Option<Option<i32>>bell: bool
Those are later turned into PaneEvent values or pending_clipboard.
CJK and grapheme width
[A comparison screenshot will be inserted here later, showing broken CJK cursor placement in another terminal and correct alignment in yatamux]
Width calculation is controlled by CjkWidthConfig.
The guiding policy is very clear:
do not trust the cursor position reported by ConPTY.
The implementation explicitly handles:
- half-width katakana voiced marks
U+FF9E/U+FF9F - East Asian Ambiguous characters
- emoji sequences containing VS-16
- cases where NFD Korean text is normalized to NFC before measuring width
When a full-width character lands at the end of a line, the code wraps early using DECAWM + LCF.
Without that logic, a two-cell grapheme can spill into the next line in a half-broken state.
Scrollback and alternate screen
Grid::SCROLLBACK_MAX is 50,000 lines,
but not every scroll is recorded.
- normal full-screen scrolls are pushed into scrollback
- subregion scrolls limited by DECSTBM are not
- alternate-screen activity is not
That preserves shell history without polluting scrollback with temporary full-screen views from vim or less.
GDI drawing pipeline
paint() in crates/client/src/window/mod.rs uses a full backbuffer approach.
- create the backbuffer with
CreateCompatibleDC/CreateCompatibleBitmap - fill the background using theme colors
- fetch the current layout and Grid references from
PaneStore - draw each pane
- draw separators, status bar, toast notifications, and launchers on top
- copy the finished frame to the window with
BitBlt
Dirty rows are mainly used by the WM_TIMER side to decide when a redraw is needed.
paint() itself currently redraws the whole visible area whenever it is called.
Why box-drawing characters are drawn directly
[A comparison screenshot will be inserted here later, showing font-dependent border glitches versus direct GDI drawing]
I do not hand all box-drawing characters over to ExtTextOutW.
Some of them are drawn with MoveToEx, LineTo, and FillRect.
The reason is simple: when those characters are left entirely to the font, border rendering in neovim and similar apps stops being stable.
By treating them as shapes rather than glyphs, the current design prioritizes UI consistency over font fidelity.
Font selection
At startup the app chooses the first available font from this priority list:
The order is a compromise between Japanese readability, Nerd Font glyph coverage, and reasonable fallback behavior on standard Windows environments.