← yatamux Technical Guide

Rendering Pipeline

Note: This chapter is a machine-translated English version of the original Japanese chapter レンダリングパイプライン. Some phrasing may read unnaturally.

Overall flow

Typst diagram

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
Typst diagram

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.

Typst diagram

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:

callbackpurpose
printwriting regular characters
executeCR / LF / BS / BEL
csi_dispatchcursor movement, erase, scroll region, SGR
osc_dispatchtitle 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.

  1. create the backbuffer with CreateCompatibleDC / CreateCompatibleBitmap
  2. fill the background using theme colors
  3. fetch the current layout and Grid references from PaneStore
  4. draw each pane
  5. draw separators, status bar, toast notifications, and launchers on top
  6. 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:

Typst diagram

The order is a compromise between Japanese readability, Nerd Font glyph coverage, and reasonable fallback behavior on standard Windows environments.