source dump of claude code
at main 124 lines 4.2 kB view raw
1import type { Cursor } from './cursor.js' 2import type { Size } from './layout/geometry.js' 3import type { ScrollHint } from './render-node-to-output.js' 4import { 5 type CharPool, 6 createScreen, 7 type HyperlinkPool, 8 type Screen, 9 type StylePool, 10} from './screen.js' 11 12export type Frame = { 13 readonly screen: Screen 14 readonly viewport: Size 15 readonly cursor: Cursor 16 /** DECSTBM scroll optimization hint (alt-screen only, null otherwise). */ 17 readonly scrollHint?: ScrollHint | null 18 /** A ScrollBox has remaining pendingScrollDelta — schedule another frame. */ 19 readonly scrollDrainPending?: boolean 20} 21 22export function emptyFrame( 23 rows: number, 24 columns: number, 25 stylePool: StylePool, 26 charPool: CharPool, 27 hyperlinkPool: HyperlinkPool, 28): Frame { 29 return { 30 screen: createScreen(0, 0, stylePool, charPool, hyperlinkPool), 31 viewport: { width: columns, height: rows }, 32 cursor: { x: 0, y: 0, visible: true }, 33 } 34} 35 36export type FlickerReason = 'resize' | 'offscreen' | 'clear' 37 38export type FrameEvent = { 39 durationMs: number 40 /** Phase breakdown in ms + patch count. Populated when the ink instance 41 * has frame-timing instrumentation enabled (via onFrame wiring). */ 42 phases?: { 43 /** createRenderer output: DOM → yoga layout → screen buffer */ 44 renderer: number 45 /** LogUpdate.render(): screen diff → Patch[] (the hot path this PR optimizes) */ 46 diff: number 47 /** optimize(): patch merge/dedupe */ 48 optimize: number 49 /** writeDiffToTerminal(): serialize patches → ANSI → stdout */ 50 write: number 51 /** Pre-optimize patch count (proxy for how much changed this frame) */ 52 patches: number 53 /** yoga calculateLayout() time (runs in resetAfterCommit, before onRender) */ 54 yoga: number 55 /** React reconcile time: scrollMutated → resetAfterCommit. 0 if no commit. */ 56 commit: number 57 /** layoutNode() calls this frame (recursive, includes cache-hit returns) */ 58 yogaVisited: number 59 /** measureFunc (text wrap/width) calls — the expensive part */ 60 yogaMeasured: number 61 /** early returns via _hasL single-slot cache */ 62 yogaCacheHits: number 63 /** total yoga Node instances alive (create - free). Growth = leak. */ 64 yogaLive: number 65 } 66 flickers: Array<{ 67 desiredHeight: number 68 availableHeight: number 69 reason: FlickerReason 70 }> 71} 72 73export type Patch = 74 | { type: 'stdout'; content: string } 75 | { type: 'clear'; count: number } 76 | { 77 type: 'clearTerminal' 78 reason: FlickerReason 79 // Populated by log-update when a scrollback diff triggers the reset. 80 // ink.tsx uses triggerY with findOwnerChainAtRow to attribute the 81 // flicker to its source React component. 82 debug?: { triggerY: number; prevLine: string; nextLine: string } 83 } 84 | { type: 'cursorHide' } 85 | { type: 'cursorShow' } 86 | { type: 'cursorMove'; x: number; y: number } 87 | { type: 'cursorTo'; col: number } 88 | { type: 'carriageReturn' } 89 | { type: 'hyperlink'; uri: string } 90 // Pre-serialized style transition string from StylePool.transition() — 91 // cached by (fromId, toId), zero allocations after warmup. 92 | { type: 'styleStr'; str: string } 93 94export type Diff = Patch[] 95 96/** 97 * Determines whether the screen should be cleared based on the current and previous frame. 98 * Returns the reason for clearing, or undefined if no clear is needed. 99 * 100 * Screen clearing is triggered when: 101 * 1. Terminal has been resized (viewport dimensions changed) → 'resize' 102 * 2. Current frame screen height exceeds available terminal rows → 'offscreen' 103 * 3. Previous frame screen height exceeded available terminal rows → 'offscreen' 104 */ 105export function shouldClearScreen( 106 prevFrame: Frame, 107 frame: Frame, 108): FlickerReason | undefined { 109 const didResize = 110 frame.viewport.height !== prevFrame.viewport.height || 111 frame.viewport.width !== prevFrame.viewport.width 112 if (didResize) { 113 return 'resize' 114 } 115 116 const currentFrameOverflows = frame.screen.height >= frame.viewport.height 117 const previousFrameOverflowed = 118 prevFrame.screen.height >= prevFrame.viewport.height 119 if (currentFrameOverflows || previousFrameOverflowed) { 120 return 'offscreen' 121 } 122 123 return undefined 124}