Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
27 kB · 773 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774import { type AnsiCode, ansiCodesToString, diffAnsiCodes,} from '@alcalzone/ansi-tokenize'import { logForDebugging } from '../utils/debug.js'import type { Diff, FlickerReason, Frame } from './frame.js'import type { Point } from './layout/geometry.js'import { type Cell, CellWidth, cellAt, charInCellAt, diffEach, type Hyperlink, isEmptyCellAt, type Screen, type StylePool, shiftRows, visibleCellAtIndex,} from './screen.js'import { CURSOR_HOME, scrollDown as csiScrollDown, scrollUp as csiScrollUp, RESET_SCROLL_REGION, setScrollRegion,} from './termio/csi.js'import { LINK_END, link as oscLink } from './termio/osc.js'
type State = { previousOutput: string}
type Options = { isTTY: boolean stylePool: StylePool}
const CARRIAGE_RETURN = { type: 'carriageReturn' } as constconst NEWLINE = { type: 'stdout', content: '\n' } as const
export class LogUpdate { private state: State
constructor(private readonly options: Options) { this.state = { previousOutput: '', } }
renderPreviousOutput_DEPRECATED(prevFrame: Frame): Diff { if (!this.options.isTTY) { // Non-TTY output is no longer supported (string output was removed) return [NEWLINE] } return this.getRenderOpsForDone(prevFrame) }
// Called when process resumes from suspension (SIGCONT) to prevent clobbering terminal content reset(): void { this.state.previousOutput = '' }
private renderFullFrame(frame: Frame): Diff { const { screen } = frame const lines: string[] = [] let currentStyles: AnsiCode[] = [] let currentHyperlink: Hyperlink = undefined for (let y = 0; y < screen.height; y++) { let line = '' for (let x = 0; x < screen.width; x++) { const cell = cellAt(screen, x, y) if (cell && cell.width !== CellWidth.SpacerTail) { // Handle hyperlink transitions if (cell.hyperlink !== currentHyperlink) { if (currentHyperlink !== undefined) { line += LINK_END } if (cell.hyperlink !== undefined) { line += oscLink(cell.hyperlink) } currentHyperlink = cell.hyperlink } const cellStyles = this.options.stylePool.get(cell.styleId) const styleDiff = diffAnsiCodes(currentStyles, cellStyles) if (styleDiff.length > 0) { line += ansiCodesToString(styleDiff) currentStyles = cellStyles } line += cell.char } } // Close any open hyperlink before resetting styles if (currentHyperlink !== undefined) { line += LINK_END currentHyperlink = undefined } // Reset styles at end of line so trimEnd doesn't leave dangling codes const resetCodes = diffAnsiCodes(currentStyles, []) if (resetCodes.length > 0) { line += ansiCodesToString(resetCodes) currentStyles = [] } lines.push(line.trimEnd()) }
if (lines.length === 0) { return [] } return [{ type: 'stdout', content: lines.join('\n') }] }
private getRenderOpsForDone(prev: Frame): Diff { this.state.previousOutput = ''
if (!prev.cursor.visible) { return [{ type: 'cursorShow' }] } return [] }
render( prev: Frame, next: Frame, altScreen = false, decstbmSafe = true, ): Diff { if (!this.options.isTTY) { return this.renderFullFrame(next) }
const startTime = performance.now() const stylePool = this.options.stylePool
// Since we assume the cursor is at the bottom on the screen, we only need // to clear when the viewport gets shorter (i.e. the cursor position drifts) // or when it gets thinner (and text wraps). We _could_ figure out how to // not reset here but that would involve predicting the current layout // _after_ the viewport change which means calcuating text wrapping. // Resizing is a rare enough event that it's not practically a big issue. if ( next.viewport.height < prev.viewport.height || (prev.viewport.width !== 0 && next.viewport.width !== prev.viewport.width) ) { return fullResetSequence_CAUSES_FLICKER(next, 'resize', stylePool) }
// DECSTBM scroll optimization: when a ScrollBox's scrollTop changed, // shift content with a hardware scroll (CSI top;bot r + CSI n S/T) // instead of rewriting the whole scroll region. The shiftRows on // prev.screen simulates the shift so the diff loop below naturally // finds only the rows that scrolled IN as diffs. prev.screen is // about to become backFrame (reused next render) so mutation is safe. // CURSOR_HOME after RESET_SCROLL_REGION is defensive — DECSTBM reset // homes cursor per spec but terminal implementations vary. // // decstbmSafe: caller passes false when the DECSTBM→diff sequence // can't be made atomic (no DEC 2026 / BSU/ESU). Without atomicity the // outer terminal renders the intermediate state — region scrolled, // edge rows not yet painted — a visible vertical jump on every frame // where scrollTop moves. Falling through to the diff loop writes all // shifted rows: more bytes, no intermediate state. next.screen from // render-node-to-output's blit+shift is correct either way. let scrollPatch: Diff = [] if (altScreen && next.scrollHint && decstbmSafe) { const { top, bottom, delta } = next.scrollHint if ( top >= 0 && bottom < prev.screen.height && bottom < next.screen.height ) { shiftRows(prev.screen, top, bottom, delta) scrollPatch = [ { type: 'stdout', content: setScrollRegion(top + 1, bottom + 1) + (delta > 0 ? csiScrollUp(delta) : csiScrollDown(-delta)) + RESET_SCROLL_REGION + CURSOR_HOME, }, ] } }
// We have to use purely relative operations to manipulate the cursor since // we don't know its starting point. // // When content height >= viewport height AND cursor is at the bottom, // the cursor restore at the end of the previous frame caused terminal scroll. // viewportY tells us how many rows are in scrollback from content overflow. // Additionally, the cursor-restore scroll pushes 1 more row into scrollback. // We need fullReset if any changes are to rows that are now in scrollback. // // This early full-reset check only applies in "steady state" (not growing). // For growing, the viewportY calculation below (with cursorRestoreScroll) // catches unreachable scrollback rows in the diff loop instead. const cursorAtBottom = prev.cursor.y >= prev.screen.height const isGrowing = next.screen.height > prev.screen.height // When content fills the viewport exactly (height == viewport) and the // cursor is at the bottom, the cursor-restore LF at the end of the // previous frame scrolled 1 row into scrollback. Use >= to catch this. const prevHadScrollback = cursorAtBottom && prev.screen.height >= prev.viewport.height const isShrinking = next.screen.height < prev.screen.height const nextFitsViewport = next.screen.height <= prev.viewport.height
// When shrinking from above-viewport to at-or-below-viewport, content that // was in scrollback should now be visible. Terminal clear operations can't // bring scrollback content into view, so we need a full reset. // Use <= (not <) because even when next height equals viewport height, the // scrollback depth from the previous render differs from a fresh render. if (prevHadScrollback && nextFitsViewport && isShrinking) { logForDebugging( `Full reset (shrink->below): prevHeight=${prev.screen.height}, nextHeight=${next.screen.height}, viewport=${prev.viewport.height}`, ) return fullResetSequence_CAUSES_FLICKER(next, 'offscreen', stylePool) }
if ( prev.screen.height >= prev.viewport.height && prev.screen.height > 0 && cursorAtBottom && !isGrowing ) { // viewportY = rows in scrollback from content overflow // +1 for the row pushed by cursor-restore scroll const viewportY = prev.screen.height - prev.viewport.height const scrollbackRows = viewportY + 1
let scrollbackChangeY = -1 diffEach(prev.screen, next.screen, (_x, y) => { if (y < scrollbackRows) { scrollbackChangeY = y return true // early exit } }) if (scrollbackChangeY >= 0) { const prevLine = readLine(prev.screen, scrollbackChangeY) const nextLine = readLine(next.screen, scrollbackChangeY) return fullResetSequence_CAUSES_FLICKER(next, 'offscreen', stylePool, { triggerY: scrollbackChangeY, prevLine, nextLine, }) } }
const screen = new VirtualScreen(prev.cursor, next.viewport.width)
// Treat empty screen as height 1 to avoid spurious adjustments on first render const heightDelta = Math.max(next.screen.height, 1) - Math.max(prev.screen.height, 1) const shrinking = heightDelta < 0 const growing = heightDelta > 0
// Handle shrinking: clear lines from the bottom if (shrinking) { const linesToClear = prev.screen.height - next.screen.height
// eraseLines only works within the viewport - it can't clear scrollback. // If we need to clear more lines than fit in the viewport, some are in // scrollback, so we need a full reset. if (linesToClear > prev.viewport.height) { return fullResetSequence_CAUSES_FLICKER( next, 'offscreen', this.options.stylePool, ) }
// clear(N) moves cursor UP by N-1 lines and to column 0 // This puts us at line prev.screen.height - N = next.screen.height // But we want to be at next.screen.height - 1 (bottom of new screen) screen.txn(prev => [ [ { type: 'clear', count: linesToClear }, { type: 'cursorMove', x: 0, y: -1 }, ], { dx: -prev.x, dy: -linesToClear }, ]) }
// viewportY = number of rows in scrollback (not visible on terminal). // For shrinking: use max(prev, next) because terminal clears don't scroll. // For growing: use prev state because new rows haven't scrolled old ones yet. // When prevHadScrollback, add 1 for the cursor-restore LF that scrolled // an additional row out of view at the end of the previous frame. Without // this, the diff loop treats that row as reachable — but the cursor clamps // at viewport top, causing writes to land 1 row off and garbling the output. const cursorRestoreScroll = prevHadScrollback ? 1 : 0 const viewportY = growing ? Math.max( 0, prev.screen.height - prev.viewport.height + cursorRestoreScroll, ) : Math.max(prev.screen.height, next.screen.height) - next.viewport.height + cursorRestoreScroll
let currentStyleId = stylePool.none let currentHyperlink: Hyperlink = undefined
// First pass: render changes to existing rows (rows < prev.screen.height) let needsFullReset = false let resetTriggerY = -1 diffEach(prev.screen, next.screen, (x, y, removed, added) => { // Skip new rows - we'll render them directly after if (growing && y >= prev.screen.height) { return }
// Skip spacers during rendering because the terminal will automatically // advance 2 columns when we write the wide character itself. // SpacerTail: Second cell of a wide character // SpacerHead: Marks line-end position where wide char wraps to next line if ( added && (added.width === CellWidth.SpacerTail || added.width === CellWidth.SpacerHead) ) { return }
if ( removed && (removed.width === CellWidth.SpacerTail || removed.width === CellWidth.SpacerHead) && !added ) { return }
// Skip empty cells that don't need to overwrite existing content. // This prevents writing trailing spaces that would cause unnecessary // line wrapping at the edge of the screen. // Uses isEmptyCellAt to check if both packed words are zero (empty cell). if (added && isEmptyCellAt(next.screen, x, y) && !removed) { return }
// If the cell outside the viewport range has changed, we need to reset // because we can't move the cursor there to draw. if (y < viewportY) { needsFullReset = true resetTriggerY = y return true // early exit }
moveCursorTo(screen, x, y)
if (added) { const targetHyperlink = added.hyperlink currentHyperlink = transitionHyperlink( screen.diff, currentHyperlink, targetHyperlink, ) const styleStr = stylePool.transition(currentStyleId, added.styleId) if (writeCellWithStyleStr(screen, added, styleStr)) { currentStyleId = added.styleId } } else if (removed) { // Cell was removed - clear it with a space // (This handles shrinking content) // Reset any active styles/hyperlinks first to avoid leaking into cleared cells const styleIdToReset = currentStyleId const hyperlinkToReset = currentHyperlink currentStyleId = stylePool.none currentHyperlink = undefined
screen.txn(() => { const patches: Diff = [] transitionStyle(patches, stylePool, styleIdToReset, stylePool.none) transitionHyperlink(patches, hyperlinkToReset, undefined) patches.push({ type: 'stdout', content: ' ' }) return [patches, { dx: 1, dy: 0 }] }) } }) if (needsFullReset) { return fullResetSequence_CAUSES_FLICKER(next, 'offscreen', stylePool, { triggerY: resetTriggerY, prevLine: readLine(prev.screen, resetTriggerY), nextLine: readLine(next.screen, resetTriggerY), }) }
// Reset styles before rendering new rows (they'll set their own styles) currentStyleId = transitionStyle( screen.diff, stylePool, currentStyleId, stylePool.none, ) currentHyperlink = transitionHyperlink( screen.diff, currentHyperlink, undefined, )
// Handle growth: render new rows directly (they naturally scroll the terminal) if (growing) { renderFrameSlice( screen, next, prev.screen.height, next.screen.height, stylePool, ) }
// Restore cursor. Skipped in alt-screen: the cursor is hidden, its // position only matters as the starting point for the NEXT frame's // relative moves, and in alt-screen the next frame always begins with // CSI H (see ink.tsx onRender) which resets to (0,0) regardless. This // saves a CR + cursorMove round-trip (~6-10 bytes) every frame. // // Main screen: if cursor needs to be past the last line of content // (typical: cursor.y = screen.height), emit \n to create that line // since cursor movement can't create new lines. if (altScreen) { // no-op; next frame's CSI H anchors cursor } else if (next.cursor.y >= next.screen.height) { // Move to column 0 of current line, then emit newlines to reach target row screen.txn(prev => { const rowsToCreate = next.cursor.y - prev.y if (rowsToCreate > 0) { // Use CR to resolve pending wrap (if any) without advancing // to the next line, then LF to create each new row. const patches: Diff = new Array<Diff[number]>(1 + rowsToCreate) patches[0] = CARRIAGE_RETURN for (let i = 0; i < rowsToCreate; i++) { patches[1 + i] = NEWLINE } return [patches, { dx: -prev.x, dy: rowsToCreate }] } // At or past target row - need to move cursor to correct position const dy = next.cursor.y - prev.y if (dy !== 0 || prev.x !== next.cursor.x) { // Use CR to clear pending wrap (if any), then cursor move const patches: Diff = [CARRIAGE_RETURN] patches.push({ type: 'cursorMove', x: next.cursor.x, y: dy }) return [patches, { dx: next.cursor.x - prev.x, dy }] } return [[], { dx: 0, dy: 0 }] }) } else { moveCursorTo(screen, next.cursor.x, next.cursor.y) }
const elapsed = performance.now() - startTime if (elapsed > 50) { const damage = next.screen.damage const damageInfo = damage ? `${damage.width}x${damage.height} at (${damage.x},${damage.y})` : 'none' logForDebugging( `Slow render: ${elapsed.toFixed(1)}ms, screen: ${next.screen.height}x${next.screen.width}, damage: ${damageInfo}, changes: ${screen.diff.length}`, ) }
return scrollPatch.length > 0 ? [...scrollPatch, ...screen.diff] : screen.diff }}
function transitionHyperlink( diff: Diff, current: Hyperlink, target: Hyperlink,): Hyperlink { if (current !== target) { diff.push({ type: 'hyperlink', uri: target ?? '' }) return target } return current}
function transitionStyle( diff: Diff, stylePool: StylePool, currentId: number, targetId: number,): number { const str = stylePool.transition(currentId, targetId) if (str.length > 0) { diff.push({ type: 'styleStr', str }) } return targetId}
function readLine(screen: Screen, y: number): string { let line = '' for (let x = 0; x < screen.width; x++) { line += charInCellAt(screen, x, y) ?? ' ' } return line.trimEnd()}
function fullResetSequence_CAUSES_FLICKER( frame: Frame, reason: FlickerReason, stylePool: StylePool, debug?: { triggerY: number; prevLine: string; nextLine: string },): Diff { // After clearTerminal, cursor is at (0, 0) const screen = new VirtualScreen({ x: 0, y: 0 }, frame.viewport.width) renderFrame(screen, frame, stylePool) return [{ type: 'clearTerminal', reason, debug }, ...screen.diff]}
function renderFrame( screen: VirtualScreen, frame: Frame, stylePool: StylePool,): void { renderFrameSlice(screen, frame, 0, frame.screen.height, stylePool)}
/** * Render a slice of rows from the frame's screen. * Each row is rendered followed by a newline. Cursor ends at (0, endY). */function renderFrameSlice( screen: VirtualScreen, frame: Frame, startY: number, endY: number, stylePool: StylePool,): VirtualScreen { let currentStyleId = stylePool.none let currentHyperlink: Hyperlink = undefined // Track the styleId of the last rendered cell on this line (-1 if none). // Passed to visibleCellAtIndex to enable fg-only space optimization. let lastRenderedStyleId = -1
const { width: screenWidth, cells, charPool, hyperlinkPool } = frame.screen
let index = startY * screenWidth for (let y = startY; y < endY; y += 1) { // Advance cursor to this row using LF (not CSI CUD / cursor-down). // CSI CUD stops at the viewport bottom margin and cannot scroll, // but LF scrolls the viewport to create new lines. Without this, // when the cursor is at the viewport bottom, moveCursorTo's // cursor-down silently fails, creating a permanent off-by-one // between the virtual cursor and the real terminal cursor. if (screen.cursor.y < y) { const rowsToAdvance = y - screen.cursor.y screen.txn(prev => { const patches: Diff = new Array<Diff[number]>(1 + rowsToAdvance) patches[0] = CARRIAGE_RETURN for (let i = 0; i < rowsToAdvance; i++) { patches[1 + i] = NEWLINE } return [patches, { dx: -prev.x, dy: rowsToAdvance }] }) } // Reset at start of each line — no cell rendered yet lastRenderedStyleId = -1
for (let x = 0; x < screenWidth; x += 1, index += 1) { // Skip spacers, unstyled empty cells, and fg-only styled spaces that // match the last rendered style (since cursor-forward produces identical // visual result). visibleCellAtIndex handles the optimization internally // to avoid allocating Cell objects for skipped cells. const cell = visibleCellAtIndex( cells, charPool, hyperlinkPool, index, lastRenderedStyleId, ) if (!cell) { continue }
moveCursorTo(screen, x, y)
// Handle hyperlink const targetHyperlink = cell.hyperlink currentHyperlink = transitionHyperlink( screen.diff, currentHyperlink, targetHyperlink, )
// Style transition — cached string, zero allocations after warmup const styleStr = stylePool.transition(currentStyleId, cell.styleId) if (writeCellWithStyleStr(screen, cell, styleStr)) { currentStyleId = cell.styleId lastRenderedStyleId = cell.styleId } } // Reset styles/hyperlinks before newline so background color doesn't // bleed into the next line when the terminal scrolls. The old code // reset implicitly by writing trailing unstyled spaces; now that we // skip empty cells, we must reset explicitly. currentStyleId = transitionStyle( screen.diff, stylePool, currentStyleId, stylePool.none, ) currentHyperlink = transitionHyperlink( screen.diff, currentHyperlink, undefined, ) // CR+LF at end of row — \r resets to column 0, \n moves to next line. // Without \r, the terminal cursor stays at whatever column content ended // (since we skip trailing spaces, this can be mid-row). screen.txn(prev => [[CARRIAGE_RETURN, NEWLINE], { dx: -prev.x, dy: 1 }]) }
// Reset any open style/hyperlink at end of slice transitionStyle(screen.diff, stylePool, currentStyleId, stylePool.none) transitionHyperlink(screen.diff, currentHyperlink, undefined)
return screen}
type Delta = { dx: number; dy: number }
/** * Write a cell with a pre-serialized style transition string (from * StylePool.transition). Inlines the txn logic to avoid closure/tuple/delta * allocations on every cell. * * Returns true if the cell was written, false if skipped (wide char at * viewport edge). Callers MUST gate currentStyleId updates on this — when * skipped, styleStr is never pushed and the terminal's style state is * unchanged. Updating the virtual tracker anyway desyncs it from the * terminal, and the next transition is computed from phantom state. */function writeCellWithStyleStr( screen: VirtualScreen, cell: Cell, styleStr: string,): boolean { const cellWidth = cell.width === CellWidth.Wide ? 2 : 1 const px = screen.cursor.x const vw = screen.viewportWidth
// Don't write wide chars that would cross the viewport edge. // Single-codepoint chars (CJK) at vw-2 are safe; multi-codepoint // graphemes (flags, ZWJ emoji) need stricter threshold. if (cellWidth === 2 && px < vw) { const threshold = cell.char.length > 2 ? vw : vw + 1 if (px + 2 >= threshold) { return false } }
const diff = screen.diff if (styleStr.length > 0) { diff.push({ type: 'styleStr', str: styleStr }) }
const needsCompensation = cellWidth === 2 && needsWidthCompensation(cell.char)
// On terminals with old wcwidth tables, a compensated emoji only advances // the cursor 1 column, so the CHA below skips column x+1 without painting // it. Write a styled space there first — on correct terminals the emoji // glyph (width 2) overwrites it harmlessly; on old terminals it fills the // gap with the emoji's background. Also clears any stale content at x+1. // CHA is 1-based, so column px+1 (0-based) is CHA target px+2. if (needsCompensation && px + 1 < vw) { diff.push({ type: 'cursorTo', col: px + 2 }) diff.push({ type: 'stdout', content: ' ' }) diff.push({ type: 'cursorTo', col: px + 1 }) }
diff.push({ type: 'stdout', content: cell.char })
// Force terminal cursor to correct column after the emoji. if (needsCompensation) { diff.push({ type: 'cursorTo', col: px + cellWidth + 1 }) }
// Update cursor — mutate in place to avoid Point allocation if (px >= vw) { screen.cursor.x = cellWidth screen.cursor.y++ } else { screen.cursor.x = px + cellWidth } return true}
function moveCursorTo(screen: VirtualScreen, targetX: number, targetY: number) { screen.txn(prev => { const dx = targetX - prev.x const dy = targetY - prev.y const inPendingWrap = prev.x >= screen.viewportWidth
// If we're in pending wrap state (cursor.x >= width), use CR // to reset to column 0 on the current line without advancing // to the next line, then issue the cursor movement. if (inPendingWrap) { return [ [CARRIAGE_RETURN, { type: 'cursorMove', x: targetX, y: dy }], { dx, dy }, ] }
// When moving to a different line, use carriage return (\r) to reset to // column 0 first, then cursor move. if (dy !== 0) { return [ [CARRIAGE_RETURN, { type: 'cursorMove', x: targetX, y: dy }], { dx, dy }, ] }
// Standard same-line cursor move return [[{ type: 'cursorMove', x: dx, y: dy }], { dx, dy }] })}
/** * Identify emoji where the terminal's wcwidth may disagree with Unicode. * On terminals with correct tables, the CHA we emit is a harmless no-op. * * Two categories: * 1. Newer emoji (Unicode 12.0+) missing from terminal wcwidth tables. * 2. Text-by-default emoji + VS16 (U+FE0F): the base codepoint is width 1 * in wcwidth, but VS16 triggers emoji presentation making it width 2. * Examples: ⚔️ (U+2694), ☠️ (U+2620), ❤️ (U+2764). */function needsWidthCompensation(char: string): boolean { const cp = char.codePointAt(0) if (cp === undefined) return false // U+1FA70-U+1FAFF: Symbols and Pictographs Extended-A (Unicode 12.0-15.0) // U+1FB00-U+1FBFF: Symbols for Legacy Computing (Unicode 13.0) if ((cp >= 0x1fa70 && cp <= 0x1faff) || (cp >= 0x1fb00 && cp <= 0x1fbff)) { return true } // Text-by-default emoji with VS16: scan for U+FE0F in multi-codepoint // graphemes. Single BMP chars (length 1) and surrogate pairs without VS16 // skip this check. VS16 (0xFE0F) can't collide with surrogates (0xD800-0xDFFF). if (char.length >= 2) { for (let i = 0; i < char.length; i++) { if (char.charCodeAt(i) === 0xfe0f) return true } } return false}
class VirtualScreen { // Public for direct mutation by writeCellWithStyleStr (avoids txn overhead). // File-private class — not exposed outside log-update.ts. cursor: Point diff: Diff = []
constructor( origin: Point, readonly viewportWidth: number, ) { this.cursor = { ...origin } }
txn(fn: (prev: Point) => [patches: Diff, next: Delta]): void { const [patches, next] = fn(this.cursor) for (const patch of patches) { this.diff.push(patch) } this.cursor.x += next.dx this.cursor.y += next.dy }}