[ Concepts ]
Frames and the Differential Renderer
The idea in plain words
Section titled “The idea in plain words”Writing the entire terminal screen to STDOUT_FILENO on every frame causes visible flicker and wastes bandwidth over SSH or terminal multiplexers. koto::Frame keeps two cell grids in memory—m_back_buffer (what your widgets draw this frame) and m_front_buffer (what is currently on the terminal screen)—and emits ANSI escape sequences only for cells that changed.
How koto implements it
Section titled “How koto implements it”- Initialization and resize invalidation:
Frameinitializesm_front_bufferwith sentinelCell{U'ÿ'}andm_back_bufferwith defaultCell{}(U' ') (src/frame.cpp:23-24, 452-456). BecauseU'ÿ'differs from a blank space, the firstrender()call (and the firstrender()afterhandle_resize()) paints every cell on the screen. - Wide-character continuation cells: When
Frame::set_cell(row, col, c, tag_id)writes a width-2 character (such as a CJK ideograph or emoji), it placesCell::make_continuation()at(row, col + 1). Ifcol + 1 >= m_cols,set_cellreplacescwithCell{U' '}of width1so the terminal never wraps mid-glyph. Overwriting either half of a width-2 character clears the companion cell to a width-1 space (src/frame.cpp:73-125). - Dirty scan and early return:
Frame::render(bool flush)scansm_back_bufferagainstm_front_buffer(ignoring cells whereback.continuation == true) and checks whether cursor visibility or position changed. If neither changed,render()returns immediately withm_outputempty (src/frame.cpp:335-353). - Synchronized output and SGR caching: When dirty cells exist,
render()appends[?2026h(DEC Mode 2026 begin synchronized update), iterates rows and columns, emits[r;cHcursor moves (skipping cursor moves only when advancing sequentially across single-width ASCII0x20..0x7Ecells), emits SGR attribute/color diffs againstm_cur_attrs,m_cur_fg, andm_cur_bg, appendsc.grapheme.view(), updatesm_front_buffer, positions or hides the hardware cursor ([?25h/[?25l), appends[?2026l, and flushesm_outputin a singlewrite_all(STDOUT_FILENO, m_output)call (src/frame.cpp:363-439).
Frame local_frame(2, 10);local_frame.set_cell(0, 0, Cell{U'A', Color::Cyan, Color::Default});local_frame.render(false);// First render emits dirty cell diff; second identical render emits 0 byteslocal_frame.render(false);Consequences for your code
Section titled “Consequences for your code”- Use
set_cellorfillwhen writing wide characters: Indexingframe[row, col]directly bypasses continuation-cell maintenance inset_cell(). - Headless testing requires no TTY: Constructing
Frame(rows, cols)or callingframe.render(false)populatesframe.raw_output()andframe.to_plain_text()in memory without touchingSTDOUT_FILENO.