@fairyhunter13/opentui-core 0.1.96 → 0.1.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  "import type { ColorInput } from \"./RGBA.js\"\n\nexport interface BorderCharacters {\n topLeft: string\n topRight: string\n bottomLeft: string\n bottomRight: string\n horizontal: string\n vertical: string\n topT: string\n bottomT: string\n leftT: string\n rightT: string\n cross: string\n}\n\nexport type BorderStyle = \"single\" | \"double\" | \"rounded\" | \"heavy\"\nexport type BorderSides = \"top\" | \"right\" | \"bottom\" | \"left\"\n\nconst VALID_BORDER_STYLES: readonly BorderStyle[] = [\"single\", \"double\", \"rounded\", \"heavy\"] as const\n\nexport function isValidBorderStyle(value: unknown): value is BorderStyle {\n return typeof value === \"string\" && VALID_BORDER_STYLES.includes(value as BorderStyle)\n}\n\nexport function parseBorderStyle(value: unknown, fallback: BorderStyle = \"single\"): BorderStyle {\n if (isValidBorderStyle(value)) {\n return value\n }\n\n if (value !== undefined && value !== null) {\n console.warn(\n `Invalid borderStyle \"${value}\", falling back to \"${fallback}\". Valid values are: ${VALID_BORDER_STYLES.join(\", \")}`,\n )\n }\n return fallback\n}\n\nexport const BorderChars: Record<BorderStyle, BorderCharacters> = {\n single: {\n topLeft: \"┌\",\n topRight: \"┐\",\n bottomLeft: \"└\",\n bottomRight: \"┘\",\n horizontal: \"─\",\n vertical: \"│\",\n topT: \"┬\",\n bottomT: \"┴\",\n leftT: \"├\",\n rightT: \"┤\",\n cross: \"┼\",\n },\n double: {\n topLeft: \"╔\",\n topRight: \"╗\",\n bottomLeft: \"╚\",\n bottomRight: \"╝\",\n horizontal: \"═\",\n vertical: \"║\",\n topT: \"╦\",\n bottomT: \"╩\",\n leftT: \"╠\",\n rightT: \"╣\",\n cross: \"╬\",\n },\n rounded: {\n topLeft: \"╭\",\n topRight: \"╮\",\n bottomLeft: \"╰\",\n bottomRight: \"╯\",\n horizontal: \"─\",\n vertical: \"│\",\n topT: \"┬\",\n bottomT: \"┴\",\n leftT: \"├\",\n rightT: \"┤\",\n cross: \"┼\",\n },\n heavy: {\n topLeft: \"┏\",\n topRight: \"┓\",\n bottomLeft: \"┗\",\n bottomRight: \"┛\",\n horizontal: \"━\",\n vertical: \"┃\",\n topT: \"┳\",\n bottomT: \"┻\",\n leftT: \"┣\",\n rightT: \"┫\",\n cross: \"╋\",\n },\n}\n\nexport interface BorderConfig {\n borderStyle: BorderStyle\n border: boolean | BorderSides[]\n borderColor?: ColorInput\n customBorderChars?: BorderCharacters\n}\n\nexport interface BoxDrawOptions {\n x: number\n y: number\n width: number\n height: number\n borderStyle: BorderStyle\n border: boolean | BorderSides[]\n borderColor: ColorInput\n customBorderChars?: BorderCharacters\n backgroundColor: ColorInput\n shouldFill?: boolean\n title?: string\n titleAlignment?: \"left\" | \"center\" | \"right\"\n}\n\nexport interface BorderSidesConfig {\n top: boolean\n right: boolean\n bottom: boolean\n left: boolean\n}\n\nexport function getBorderFromSides(sides: BorderSidesConfig): boolean | BorderSides[] {\n const result: BorderSides[] = []\n if (sides.top) result.push(\"top\")\n if (sides.right) result.push(\"right\")\n if (sides.bottom) result.push(\"bottom\")\n if (sides.left) result.push(\"left\")\n return result.length > 0 ? result : false\n}\n\nexport function getBorderSides(border: boolean | BorderSides[]): BorderSidesConfig {\n return border === true\n ? { top: true, right: true, bottom: true, left: true }\n : Array.isArray(border)\n ? {\n top: border.includes(\"top\"),\n right: border.includes(\"right\"),\n bottom: border.includes(\"bottom\"),\n left: border.includes(\"left\"),\n }\n : { top: false, right: false, bottom: false, left: false }\n}\n\n// Convert BorderCharacters to Uint32Array for passing to Zig\nexport function borderCharsToArray(chars: BorderCharacters): Uint32Array {\n const array = new Uint32Array(11)\n array[0] = chars.topLeft.codePointAt(0)!\n array[1] = chars.topRight.codePointAt(0)!\n array[2] = chars.bottomLeft.codePointAt(0)!\n array[3] = chars.bottomRight.codePointAt(0)!\n array[4] = chars.horizontal.codePointAt(0)!\n array[5] = chars.vertical.codePointAt(0)!\n array[6] = chars.topT.codePointAt(0)!\n array[7] = chars.bottomT.codePointAt(0)!\n array[8] = chars.leftT.codePointAt(0)!\n array[9] = chars.rightT.codePointAt(0)!\n array[10] = chars.cross.codePointAt(0)!\n return array\n}\n\n// Pre-converted border character arrays for performance\nexport const BorderCharArrays: Record<BorderStyle, Uint32Array> = {\n single: borderCharsToArray(BorderChars.single),\n double: borderCharsToArray(BorderChars.double),\n rounded: borderCharsToArray(BorderChars.rounded),\n heavy: borderCharsToArray(BorderChars.heavy),\n}\n",
10
10
  "import { EventEmitter } from \"events\"\nimport { type KeyEventType, type ParsedKey } from \"./parse.keypress.js\"\nimport type { PasteMetadata } from \"./paste.js\"\n\nexport class KeyEvent implements ParsedKey {\n name: string\n ctrl: boolean\n meta: boolean\n shift: boolean\n option: boolean\n sequence: string\n number: boolean\n raw: string\n eventType: KeyEventType\n source: \"raw\" | \"kitty\"\n code?: string\n super?: boolean\n hyper?: boolean\n capsLock?: boolean\n numLock?: boolean\n baseCode?: number\n repeated?: boolean\n\n private _defaultPrevented: boolean = false\n private _propagationStopped: boolean = false\n\n constructor(key: ParsedKey) {\n this.name = key.name\n this.ctrl = key.ctrl\n this.meta = key.meta\n this.shift = key.shift\n this.option = key.option\n this.sequence = key.sequence\n this.number = key.number\n this.raw = key.raw\n this.eventType = key.eventType\n this.source = key.source\n this.code = key.code\n this.super = key.super\n this.hyper = key.hyper\n this.capsLock = key.capsLock\n this.numLock = key.numLock\n this.baseCode = key.baseCode\n this.repeated = key.repeated\n }\n\n get defaultPrevented(): boolean {\n return this._defaultPrevented\n }\n\n get propagationStopped(): boolean {\n return this._propagationStopped\n }\n\n preventDefault(): void {\n this._defaultPrevented = true\n }\n\n stopPropagation(): void {\n this._propagationStopped = true\n }\n}\n\nexport class PasteEvent {\n type = \"paste\" as const\n bytes: Uint8Array\n metadata?: PasteMetadata\n private _defaultPrevented: boolean = false\n private _propagationStopped: boolean = false\n\n constructor(bytes: Uint8Array, metadata?: PasteMetadata) {\n this.bytes = bytes\n this.metadata = metadata\n }\n\n get defaultPrevented(): boolean {\n return this._defaultPrevented\n }\n\n get propagationStopped(): boolean {\n return this._propagationStopped\n }\n\n preventDefault(): void {\n this._defaultPrevented = true\n }\n\n stopPropagation(): void {\n this._propagationStopped = true\n }\n}\n\nexport type KeyHandlerEventMap = {\n keypress: [KeyEvent]\n keyrelease: [KeyEvent]\n paste: [PasteEvent]\n}\n\nexport class KeyHandler extends EventEmitter<KeyHandlerEventMap> {\n public processParsedKey(parsedKey: ParsedKey): boolean {\n try {\n switch (parsedKey.eventType) {\n case \"press\":\n this.emit(\"keypress\", new KeyEvent(parsedKey))\n break\n case \"release\":\n this.emit(\"keyrelease\", new KeyEvent(parsedKey))\n break\n default:\n this.emit(\"keypress\", new KeyEvent(parsedKey))\n break\n }\n } catch (error) {\n console.error(`[KeyHandler] Error processing parsed key:`, error)\n return true\n }\n\n return true\n }\n\n public processPaste(bytes: Uint8Array, metadata?: PasteMetadata): void {\n try {\n this.emit(\"paste\", new PasteEvent(bytes, metadata))\n } catch (error) {\n console.error(`[KeyHandler] Error processing paste:`, error)\n }\n }\n}\n\n/**\n * This class is used internally by the renderer to ensure global handlers\n * can preventDefault before renderable handlers process events.\n */\nexport class InternalKeyHandler extends KeyHandler {\n private renderableHandlers: Map<keyof KeyHandlerEventMap, Set<Function>> = new Map()\n\n public override emit<K extends keyof KeyHandlerEventMap>(event: K, ...args: KeyHandlerEventMap[K]): boolean {\n return this.emitWithPriority(event, ...args)\n }\n\n private emitWithPriority<K extends keyof KeyHandlerEventMap>(event: K, ...args: KeyHandlerEventMap[K]): boolean {\n let hasGlobalListeners = false\n\n // Check if we should emit to global handlers\n // Global handlers are emitted using the parent EventEmitter which calls all listeners\n // We need to manually iterate to check for stopPropagation between handlers\n const globalListeners = this.listeners(event as any)\n if (globalListeners.length > 0) {\n hasGlobalListeners = true\n\n for (const listener of globalListeners) {\n try {\n listener(...args)\n } catch (error) {\n console.error(`[KeyHandler] Error in global ${event} handler:`, error)\n }\n\n // Check if propagation was stopped after this handler\n if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n const keyEvent = args[0]\n if (keyEvent.propagationStopped) {\n return hasGlobalListeners\n }\n }\n }\n }\n\n const renderableSet = this.renderableHandlers.get(event)\n // Snapshot the handler list so listeners added during dispatch (e.g., via focus changes)\n // do not receive the in-flight key event.\n const renderableHandlers = renderableSet && renderableSet.size > 0 ? [...renderableSet] : []\n let hasRenderableListeners = false\n\n if (renderableSet && renderableSet.size > 0) {\n hasRenderableListeners = true\n\n if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n const keyEvent = args[0]\n if (keyEvent.defaultPrevented) return hasGlobalListeners || hasRenderableListeners\n if (keyEvent.propagationStopped) return hasGlobalListeners || hasRenderableListeners\n }\n\n for (const handler of renderableHandlers) {\n try {\n handler(...args)\n } catch (error) {\n console.error(`[KeyHandler] Error in renderable ${event} handler:`, error)\n }\n\n // Check if propagation was stopped after this handler\n if (event === \"keypress\" || event === \"keyrelease\" || event === \"paste\") {\n const keyEvent = args[0]\n if (keyEvent.propagationStopped) {\n return hasGlobalListeners || hasRenderableListeners\n }\n }\n }\n }\n\n return hasGlobalListeners || hasRenderableListeners\n }\n\n public onInternal<K extends keyof KeyHandlerEventMap>(\n event: K,\n handler: (...args: KeyHandlerEventMap[K]) => void,\n ): void {\n if (!this.renderableHandlers.has(event)) {\n this.renderableHandlers.set(event, new Set())\n }\n this.renderableHandlers.get(event)!.add(handler)\n }\n\n public offInternal<K extends keyof KeyHandlerEventMap>(\n event: K,\n handler: (...args: KeyHandlerEventMap[K]) => void,\n ): void {\n const handlers = this.renderableHandlers.get(event)\n if (handlers) {\n handlers.delete(handler)\n }\n }\n}\n",
11
11
  "export class RGBA {\n buffer: Float32Array\n\n constructor(buffer: Float32Array) {\n this.buffer = buffer\n }\n\n static fromArray(array: Float32Array) {\n return new RGBA(array)\n }\n\n static fromValues(r: number, g: number, b: number, a: number = 1.0) {\n return new RGBA(new Float32Array([r, g, b, a]))\n }\n\n static fromInts(r: number, g: number, b: number, a: number = 255) {\n return new RGBA(new Float32Array([r / 255, g / 255, b / 255, a / 255]))\n }\n\n static fromHex(hex: string): RGBA {\n return hexToRgb(hex)\n }\n\n toInts(): [number, number, number, number] {\n return [Math.round(this.r * 255), Math.round(this.g * 255), Math.round(this.b * 255), Math.round(this.a * 255)]\n }\n\n get r(): number {\n return this.buffer[0]\n }\n\n set r(value: number) {\n this.buffer[0] = value\n }\n\n get g(): number {\n return this.buffer[1]\n }\n\n set g(value: number) {\n this.buffer[1] = value\n }\n\n get b(): number {\n return this.buffer[2]\n }\n\n set b(value: number) {\n this.buffer[2] = value\n }\n\n get a(): number {\n return this.buffer[3]\n }\n\n set a(value: number) {\n this.buffer[3] = value\n }\n\n map<R>(fn: (value: number) => R) {\n return [fn(this.r), fn(this.g), fn(this.b), fn(this.a)]\n }\n\n toString() {\n return `rgba(${this.r.toFixed(2)}, ${this.g.toFixed(2)}, ${this.b.toFixed(2)}, ${this.a.toFixed(2)})`\n }\n\n equals(other?: RGBA): boolean {\n if (!other) return false\n return this.r === other.r && this.g === other.g && this.b === other.b && this.a === other.a\n }\n}\n\nexport type ColorInput = string | RGBA\n\nexport function hexToRgb(hex: string): RGBA {\n hex = hex.replace(/^#/, \"\")\n\n if (hex.length === 3) {\n hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]\n } else if (hex.length === 4) {\n hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]\n }\n\n if (!/^[0-9A-Fa-f]{6}$/.test(hex) && !/^[0-9A-Fa-f]{8}$/.test(hex)) {\n console.warn(`Invalid hex color: ${hex}, defaulting to magenta`)\n return RGBA.fromValues(1, 0, 1, 1)\n }\n\n const r = parseInt(hex.substring(0, 2), 16) / 255\n const g = parseInt(hex.substring(2, 4), 16) / 255\n const b = parseInt(hex.substring(4, 6), 16) / 255\n const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) / 255 : 1\n\n return RGBA.fromValues(r, g, b, a)\n}\n\nexport function rgbToHex(rgb: RGBA): string {\n const components = rgb.a === 1 ? [rgb.r, rgb.g, rgb.b] : [rgb.r, rgb.g, rgb.b, rgb.a]\n return (\n \"#\" +\n components\n .map((x) => {\n const hex = Math.floor(Math.max(0, Math.min(1, x) * 255)).toString(16)\n return hex.length === 1 ? \"0\" + hex : hex\n })\n .join(\"\")\n )\n}\n\nexport function hsvToRgb(h: number, s: number, v: number): RGBA {\n let r = 0,\n g = 0,\n b = 0\n\n const i = Math.floor(h / 60) % 6\n const f = h / 60 - Math.floor(h / 60)\n const p = v * (1 - s)\n const q = v * (1 - f * s)\n const t = v * (1 - (1 - f) * s)\n\n switch (i) {\n case 0:\n r = v\n g = t\n b = p\n break\n case 1:\n r = q\n g = v\n b = p\n break\n case 2:\n r = p\n g = v\n b = t\n break\n case 3:\n r = p\n g = q\n b = v\n break\n case 4:\n r = t\n g = p\n b = v\n break\n case 5:\n r = v\n g = p\n b = q\n break\n }\n\n return RGBA.fromValues(r, g, b, 1)\n}\n\nconst CSS_COLOR_NAMES: Record<string, string> = {\n black: \"#000000\",\n white: \"#FFFFFF\",\n red: \"#FF0000\",\n green: \"#008000\",\n blue: \"#0000FF\",\n yellow: \"#FFFF00\",\n cyan: \"#00FFFF\",\n magenta: \"#FF00FF\",\n silver: \"#C0C0C0\",\n gray: \"#808080\",\n grey: \"#808080\",\n maroon: \"#800000\",\n olive: \"#808000\",\n lime: \"#00FF00\",\n aqua: \"#00FFFF\",\n teal: \"#008080\",\n navy: \"#000080\",\n fuchsia: \"#FF00FF\",\n purple: \"#800080\",\n orange: \"#FFA500\",\n brightblack: \"#666666\",\n brightred: \"#FF6666\",\n brightgreen: \"#66FF66\",\n brightblue: \"#6666FF\",\n brightyellow: \"#FFFF66\",\n brightcyan: \"#66FFFF\",\n brightmagenta: \"#FF66FF\",\n brightwhite: \"#FFFFFF\",\n}\n\nexport function parseColor(color: ColorInput): RGBA {\n if (typeof color === \"string\") {\n const lowerColor = color.toLowerCase()\n\n if (lowerColor === \"transparent\") {\n return RGBA.fromValues(0, 0, 0, 0)\n }\n\n if (CSS_COLOR_NAMES[lowerColor]) {\n return hexToRgb(CSS_COLOR_NAMES[lowerColor])\n }\n\n return hexToRgb(color)\n }\n return color\n}\n",
12
- "import { OptimizedBuffer } from \"../buffer.js\"\nimport { parseColor, RGBA, type ColorInput } from \"./RGBA.js\"\nimport block from \"./fonts/block.json\"\nimport shade from \"./fonts/shade.json\"\nimport slick from \"./fonts/slick.json\"\nimport tiny from \"./fonts/tiny.json\"\nimport huge from \"./fonts/huge.json\"\nimport grid from \"./fonts/grid.json\"\nimport pallet from \"./fonts/pallet.json\"\n\n/*\n * Renders ASCII fonts to a buffer.\n * Font definitions plugged from cfonts - https://github.com/dominikwilkowski/cfonts\n */\n\nexport type ASCIIFontName = \"tiny\" | \"block\" | \"shade\" | \"slick\" | \"huge\" | \"grid\" | \"pallet\"\n\nexport const fonts = {\n tiny,\n block,\n shade,\n slick,\n huge,\n grid,\n pallet,\n}\n\ntype FontSegment = {\n text: string\n colorIndex: number\n}\n\ntype FontDefinition = {\n name: string\n lines: number\n letterspace_size: number\n letterspace: string[]\n colors?: number\n chars: Record<string, string[]>\n}\n\ntype ParsedFontDefinition = {\n name: string\n lines: number\n letterspace_size: number\n letterspace: string[]\n colors: number\n chars: Record<string, FontSegment[][]>\n}\n\nconst parsedFonts: Record<string, ParsedFontDefinition> = {}\n\nfunction parseColorTags(text: string): FontSegment[] {\n const segments: FontSegment[] = []\n let currentIndex = 0\n\n const colorTagRegex = /<c(\\d+)>(.*?)<\\/c\\d+>/g\n let lastIndex = 0\n let match\n\n while ((match = colorTagRegex.exec(text)) !== null) {\n if (match.index > lastIndex) {\n const plainText = text.slice(lastIndex, match.index)\n if (plainText) {\n segments.push({ text: plainText, colorIndex: 0 })\n }\n }\n\n const colorIndex = parseInt(match[1]) - 1\n const taggedText = match[2]\n segments.push({ text: taggedText, colorIndex: Math.max(0, colorIndex) })\n\n lastIndex = match.index + match[0].length\n }\n\n if (lastIndex < text.length) {\n const remainingText = text.slice(lastIndex)\n if (remainingText) {\n segments.push({ text: remainingText, colorIndex: 0 })\n }\n }\n\n return segments\n}\n\nfunction getParsedFont(fontKey: keyof typeof fonts): ParsedFontDefinition {\n if (!parsedFonts[fontKey]) {\n const fontDef = fonts[fontKey] as FontDefinition\n const parsedChars: Record<string, FontSegment[][]> = {}\n\n for (const [char, lines] of Object.entries(fontDef.chars)) {\n parsedChars[char] = lines.map((line) => parseColorTags(line))\n }\n\n parsedFonts[fontKey] = {\n ...fontDef,\n colors: fontDef.colors || 1,\n chars: parsedChars,\n }\n }\n\n return parsedFonts[fontKey]\n}\n\nexport function measureText({ text, font = \"tiny\" }: { text: string; font?: keyof typeof fonts }): {\n width: number\n height: number\n} {\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n console.warn(`Font '${font}' not found`)\n return { width: 0, height: 0 }\n }\n\n let currentX = 0\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n let spaceWidth = 0\n for (const segment of spaceChar[0]) {\n spaceWidth += segment.text.length\n }\n currentX += spaceWidth\n } else {\n currentX += 1\n }\n continue\n }\n\n let charWidth = 0\n if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n }\n\n return {\n width: currentX,\n height: fontDef.lines,\n }\n}\n\nexport function getCharacterPositions(text: string, font: keyof typeof fonts = \"tiny\"): number[] {\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n return [0]\n }\n\n const positions: number[] = [0]\n let currentX = 0\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n let charWidth = 0\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n for (const segment of spaceChar[0]) {\n charWidth += segment.text.length\n }\n } else {\n charWidth = 1\n }\n } else if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n\n positions.push(currentX)\n }\n\n return positions\n}\n\nexport function coordinateToCharacterIndex(x: number, text: string, font: keyof typeof fonts = \"tiny\"): number {\n const positions = getCharacterPositions(text, font)\n\n if (x < 0) {\n return 0\n }\n\n for (let i = 0; i < positions.length - 1; i++) {\n const currentPos = positions[i]\n const nextPos = positions[i + 1]\n\n if (x >= currentPos && x < nextPos) {\n const charMidpoint = currentPos + (nextPos - currentPos) / 2\n return x < charMidpoint ? i : i + 1\n }\n }\n\n if (positions.length > 0 && x >= positions[positions.length - 1]) {\n return text.length\n }\n\n return 0\n}\n\nexport function renderFontToFrameBuffer(\n buffer: OptimizedBuffer,\n {\n text,\n x = 0,\n y = 0,\n color = [RGBA.fromInts(255, 255, 255, 255)],\n backgroundColor = RGBA.fromInts(0, 0, 0, 255),\n font = \"tiny\",\n }: {\n text: string\n x?: number\n y?: number\n color?: ColorInput | ColorInput[]\n backgroundColor?: ColorInput\n font?: keyof typeof fonts\n },\n): { width: number; height: number } {\n const width = buffer.width\n const height = buffer.height\n\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n console.warn(`Font '${font}' not found`)\n return { width: 0, height: 0 }\n }\n\n const colors = Array.isArray(color) ? color : [color]\n\n if (y < 0 || y + fontDef.lines > height) {\n return { width: 0, height: fontDef.lines }\n }\n\n let currentX = x\n const startX = x\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n let spaceWidth = 0\n for (const segment of spaceChar[0]) {\n spaceWidth += segment.text.length\n }\n currentX += spaceWidth\n } else {\n currentX += 1\n }\n continue\n }\n\n let charWidth = 0\n if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n if (currentX >= width) break\n if (currentX + charWidth < 0) {\n currentX += charWidth + fontDef.letterspace_size\n continue\n }\n\n for (let lineIdx = 0; lineIdx < fontDef.lines && lineIdx < charDef.length; lineIdx++) {\n const segments = charDef[lineIdx]\n const renderY = y + lineIdx\n\n if (renderY >= 0 && renderY < height) {\n let segmentX = currentX\n\n for (const segment of segments) {\n const segmentColor = colors[segment.colorIndex] || colors[0]\n\n for (let charIdx = 0; charIdx < segment.text.length; charIdx++) {\n const renderX = segmentX + charIdx\n\n if (renderX >= 0 && renderX < width) {\n const fontChar = segment.text[charIdx]\n if (fontChar !== \" \") {\n buffer.setCellWithAlphaBlending(\n renderX,\n renderY,\n fontChar,\n parseColor(segmentColor),\n parseColor(backgroundColor),\n )\n }\n }\n }\n\n segmentX += segment.text.length\n }\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n }\n\n return {\n width: currentX - startX,\n height: fontDef.lines,\n }\n}\n",
12
+ "import { OptimizedBuffer } from \"../buffer.js\"\nimport { parseColor, RGBA, type ColorInput } from \"./RGBA.js\"\nimport block from \"./fonts/block.json\" with { type: \"json\" }\nimport shade from \"./fonts/shade.json\" with { type: \"json\" }\nimport slick from \"./fonts/slick.json\" with { type: \"json\" }\nimport tiny from \"./fonts/tiny.json\" with { type: \"json\" }\nimport huge from \"./fonts/huge.json\" with { type: \"json\" }\nimport grid from \"./fonts/grid.json\" with { type: \"json\" }\nimport pallet from \"./fonts/pallet.json\" with { type: \"json\" }\n\n/*\n * Renders ASCII fonts to a buffer.\n * Font definitions plugged from cfonts - https://github.com/dominikwilkowski/cfonts\n */\n\nexport type ASCIIFontName = \"tiny\" | \"block\" | \"shade\" | \"slick\" | \"huge\" | \"grid\" | \"pallet\"\n\nexport const fonts = {\n tiny,\n block,\n shade,\n slick,\n huge,\n grid,\n pallet,\n}\n\ntype FontSegment = {\n text: string\n colorIndex: number\n}\n\ntype FontDefinition = {\n name: string\n lines: number\n letterspace_size: number\n letterspace: string[]\n colors?: number\n chars: Record<string, string[]>\n}\n\ntype ParsedFontDefinition = {\n name: string\n lines: number\n letterspace_size: number\n letterspace: string[]\n colors: number\n chars: Record<string, FontSegment[][]>\n}\n\nconst parsedFonts: Record<string, ParsedFontDefinition> = {}\n\nfunction parseColorTags(text: string): FontSegment[] {\n const segments: FontSegment[] = []\n let currentIndex = 0\n\n const colorTagRegex = /<c(\\d+)>(.*?)<\\/c\\d+>/g\n let lastIndex = 0\n let match\n\n while ((match = colorTagRegex.exec(text)) !== null) {\n if (match.index > lastIndex) {\n const plainText = text.slice(lastIndex, match.index)\n if (plainText) {\n segments.push({ text: plainText, colorIndex: 0 })\n }\n }\n\n const colorIndex = parseInt(match[1]) - 1\n const taggedText = match[2]\n segments.push({ text: taggedText, colorIndex: Math.max(0, colorIndex) })\n\n lastIndex = match.index + match[0].length\n }\n\n if (lastIndex < text.length) {\n const remainingText = text.slice(lastIndex)\n if (remainingText) {\n segments.push({ text: remainingText, colorIndex: 0 })\n }\n }\n\n return segments\n}\n\nfunction getParsedFont(fontKey: keyof typeof fonts): ParsedFontDefinition {\n if (!parsedFonts[fontKey]) {\n const fontDef = fonts[fontKey] as FontDefinition\n const parsedChars: Record<string, FontSegment[][]> = {}\n\n for (const [char, lines] of Object.entries(fontDef.chars)) {\n parsedChars[char] = lines.map((line) => parseColorTags(line))\n }\n\n parsedFonts[fontKey] = {\n ...fontDef,\n colors: fontDef.colors || 1,\n chars: parsedChars,\n }\n }\n\n return parsedFonts[fontKey]\n}\n\nexport function measureText({ text, font = \"tiny\" }: { text: string; font?: keyof typeof fonts }): {\n width: number\n height: number\n} {\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n console.warn(`Font '${font}' not found`)\n return { width: 0, height: 0 }\n }\n\n let currentX = 0\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n let spaceWidth = 0\n for (const segment of spaceChar[0]) {\n spaceWidth += segment.text.length\n }\n currentX += spaceWidth\n } else {\n currentX += 1\n }\n continue\n }\n\n let charWidth = 0\n if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n }\n\n return {\n width: currentX,\n height: fontDef.lines,\n }\n}\n\nexport function getCharacterPositions(text: string, font: keyof typeof fonts = \"tiny\"): number[] {\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n return [0]\n }\n\n const positions: number[] = [0]\n let currentX = 0\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n let charWidth = 0\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n for (const segment of spaceChar[0]) {\n charWidth += segment.text.length\n }\n } else {\n charWidth = 1\n }\n } else if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n\n positions.push(currentX)\n }\n\n return positions\n}\n\nexport function coordinateToCharacterIndex(x: number, text: string, font: keyof typeof fonts = \"tiny\"): number {\n const positions = getCharacterPositions(text, font)\n\n if (x < 0) {\n return 0\n }\n\n for (let i = 0; i < positions.length - 1; i++) {\n const currentPos = positions[i]\n const nextPos = positions[i + 1]\n\n if (x >= currentPos && x < nextPos) {\n const charMidpoint = currentPos + (nextPos - currentPos) / 2\n return x < charMidpoint ? i : i + 1\n }\n }\n\n if (positions.length > 0 && x >= positions[positions.length - 1]) {\n return text.length\n }\n\n return 0\n}\n\nexport function renderFontToFrameBuffer(\n buffer: OptimizedBuffer,\n {\n text,\n x = 0,\n y = 0,\n color = [RGBA.fromInts(255, 255, 255, 255)],\n backgroundColor = RGBA.fromInts(0, 0, 0, 255),\n font = \"tiny\",\n }: {\n text: string\n x?: number\n y?: number\n color?: ColorInput | ColorInput[]\n backgroundColor?: ColorInput\n font?: keyof typeof fonts\n },\n): { width: number; height: number } {\n const width = buffer.width\n const height = buffer.height\n\n const fontDef = getParsedFont(font)\n if (!fontDef) {\n console.warn(`Font '${font}' not found`)\n return { width: 0, height: 0 }\n }\n\n const colors = Array.isArray(color) ? color : [color]\n\n if (y < 0 || y + fontDef.lines > height) {\n return { width: 0, height: fontDef.lines }\n }\n\n let currentX = x\n const startX = x\n\n for (let i = 0; i < text.length; i++) {\n const char = text[i].toUpperCase()\n const charDef = fontDef.chars[char]\n\n if (!charDef) {\n const spaceChar = fontDef.chars[\" \"]\n if (spaceChar && spaceChar[0]) {\n let spaceWidth = 0\n for (const segment of spaceChar[0]) {\n spaceWidth += segment.text.length\n }\n currentX += spaceWidth\n } else {\n currentX += 1\n }\n continue\n }\n\n let charWidth = 0\n if (charDef[0]) {\n for (const segment of charDef[0]) {\n charWidth += segment.text.length\n }\n }\n\n if (currentX >= width) break\n if (currentX + charWidth < 0) {\n currentX += charWidth + fontDef.letterspace_size\n continue\n }\n\n for (let lineIdx = 0; lineIdx < fontDef.lines && lineIdx < charDef.length; lineIdx++) {\n const segments = charDef[lineIdx]\n const renderY = y + lineIdx\n\n if (renderY >= 0 && renderY < height) {\n let segmentX = currentX\n\n for (const segment of segments) {\n const segmentColor = colors[segment.colorIndex] || colors[0]\n\n for (let charIdx = 0; charIdx < segment.text.length; charIdx++) {\n const renderX = segmentX + charIdx\n\n if (renderX >= 0 && renderX < width) {\n const fontChar = segment.text[charIdx]\n if (fontChar !== \" \") {\n buffer.setCellWithAlphaBlending(\n renderX,\n renderY,\n fontChar,\n parseColor(segmentColor),\n parseColor(backgroundColor),\n )\n }\n }\n }\n\n segmentX += segment.text.length\n }\n }\n }\n\n currentX += charWidth\n\n if (i < text.length - 1) {\n currentX += fontDef.letterspace_size\n }\n }\n\n return {\n width: currentX - startX,\n height: fontDef.lines,\n }\n}\n",
13
13
  "import type { RGBA } from \"./lib/RGBA.js\"\nimport type { EventEmitter } from \"events\"\nimport type { Selection } from \"./lib/selection.js\"\nimport type { Renderable } from \"./Renderable.js\"\nimport type { InternalKeyHandler, KeyHandler } from \"./lib/KeyHandler.js\"\n\nexport const TextAttributes = {\n NONE: 0,\n BOLD: 1 << 0, // 1\n DIM: 1 << 1, // 2\n ITALIC: 1 << 2, // 4\n UNDERLINE: 1 << 3, // 8\n BLINK: 1 << 4, // 16\n INVERSE: 1 << 5, // 32\n HIDDEN: 1 << 6, // 64\n STRIKETHROUGH: 1 << 7, // 128\n}\n\n// Constants for attribute bit packing\nexport const ATTRIBUTE_BASE_BITS = 8\nexport const ATTRIBUTE_BASE_MASK = 0xff\n\n/**\n * Extract the base 8 bits of attributes from a u32 attribute value.\n * Currently we only use the first 8 bits for standard text attributes.\n */\nexport function getBaseAttributes(attr: number): number {\n return attr & ATTRIBUTE_BASE_MASK\n}\n\nexport type ThemeMode = \"dark\" | \"light\"\n\nexport type CursorStyle = \"block\" | \"line\" | \"underline\" | \"default\"\n\nexport type MousePointerStyle = \"default\" | \"pointer\" | \"text\" | \"crosshair\" | \"move\" | \"not-allowed\"\n\nexport interface CursorStyleOptions {\n style?: CursorStyle\n blinking?: boolean\n color?: RGBA\n cursor?: MousePointerStyle\n}\n\nexport enum DebugOverlayCorner {\n topLeft = 0,\n topRight = 1,\n bottomLeft = 2,\n bottomRight = 3,\n}\n\nexport enum TargetChannel {\n FG = 1,\n BG = 2,\n Both = 3,\n}\n\nexport type WidthMethod = \"wcwidth\" | \"unicode\"\n\nexport interface RendererEvents {\n resize: (width: number, height: number) => void\n key: (data: Buffer) => void\n \"memory:snapshot\": (snapshot: { heapUsed: number; heapTotal: number; arrayBuffers: number }) => void\n selection: (selection: Selection) => void\n \"debugOverlay:toggle\": (enabled: boolean) => void\n theme_mode: (mode: ThemeMode) => void\n}\n\nexport interface RenderContext extends EventEmitter {\n addToHitGrid: (x: number, y: number, width: number, height: number, id: number) => void\n pushHitGridScissorRect: (x: number, y: number, width: number, height: number) => void\n popHitGridScissorRect: () => void\n clearHitGridScissorRects: () => void\n width: number\n height: number\n requestRender: () => void\n setCursorPosition: (x: number, y: number, visible: boolean) => void\n setCursorStyle: (options: CursorStyleOptions) => void\n setCursorColor: (color: RGBA) => void\n setMousePointer: (shape: MousePointerStyle) => void\n widthMethod: WidthMethod\n capabilities: any | null\n requestLive: () => void\n dropLive: () => void\n hasSelection: boolean\n getSelection: () => Selection | null\n requestSelectionUpdate: () => void\n currentFocusedRenderable: Renderable | null\n focusRenderable: (renderable: Renderable) => void\n registerLifecyclePass: (renderable: Renderable) => void\n unregisterLifecyclePass: (renderable: Renderable) => void\n getLifecyclePasses: () => Set<Renderable>\n keyInput: KeyHandler\n _internalKeyInput: InternalKeyHandler\n clearSelection: () => void\n startSelection: (renderable: Renderable, x: number, y: number) => void\n updateSelection: (\n currentRenderable: Renderable | undefined,\n x: number,\n y: number,\n options?: { finishDragging?: boolean },\n ) => void\n}\n\nexport type Timeout = ReturnType<typeof setTimeout> | undefined\n\nexport interface ViewportBounds {\n x: number\n y: number\n width: number\n height: number\n}\n\nexport interface Highlight {\n start: number\n end: number\n styleId: number\n priority?: number | null\n hlRef?: number | null\n}\n\nexport interface LineInfo {\n /** Display-column offset for each visual line start. */\n lineStartCols: number[]\n /** Display-column width for each visual line. */\n lineWidthCols: number[]\n /** Maximum display-column width across the reported lines. */\n lineWidthColsMax: number\n /** Source logical line index for each visual line. */\n lineSources: number[]\n /** Wrap index within each source logical line. */\n lineWraps: number[]\n}\n\nexport interface LineInfoProvider {\n get lineInfo(): LineInfo\n get lineCount(): number\n get virtualLineCount(): number\n get scrollY(): number\n}\n\nexport interface CapturedSpan {\n text: string\n fg: RGBA\n bg: RGBA\n attributes: number\n width: number\n}\n\nexport interface CapturedLine {\n spans: CapturedSpan[]\n}\n\nexport interface CapturedFrame {\n cols: number\n rows: number\n cursor: [number, number]\n lines: CapturedLine[]\n}\n",
14
14
  "import { TextAttributes } from \"./types.js\"\nimport { Renderable } from \"./Renderable.js\"\n\nexport function createTextAttributes({\n bold = false,\n italic = false,\n underline = false,\n dim = false,\n blink = false,\n inverse = false,\n hidden = false,\n strikethrough = false,\n}: {\n bold?: boolean\n italic?: boolean\n underline?: boolean\n dim?: boolean\n blink?: boolean\n inverse?: boolean\n hidden?: boolean\n strikethrough?: boolean\n} = {}): number {\n let attributes = TextAttributes.NONE\n\n if (bold) attributes |= TextAttributes.BOLD\n if (italic) attributes |= TextAttributes.ITALIC\n if (underline) attributes |= TextAttributes.UNDERLINE\n if (dim) attributes |= TextAttributes.DIM\n if (blink) attributes |= TextAttributes.BLINK\n if (inverse) attributes |= TextAttributes.INVERSE\n if (hidden) attributes |= TextAttributes.HIDDEN\n if (strikethrough) attributes |= TextAttributes.STRIKETHROUGH\n\n return attributes\n}\n\n// Link attribute helpers (bits 8-31 encode link_id)\nconst ATTRIBUTE_BASE_MASK = 0xff\nconst LINK_ID_SHIFT = 8\nconst LINK_ID_PAYLOAD_MASK = 0xffffff\n\nexport function attributesWithLink(baseAttributes: number, linkId: number): number {\n const base = baseAttributes & ATTRIBUTE_BASE_MASK\n const linkBits = (linkId & LINK_ID_PAYLOAD_MASK) << LINK_ID_SHIFT\n return base | linkBits\n}\n\nexport function getLinkId(attributes: number): number {\n return (attributes >>> LINK_ID_SHIFT) & LINK_ID_PAYLOAD_MASK\n}\n\n// For debugging purposes\nexport function visualizeRenderableTree(renderable: Renderable, maxDepth: number = 10): void {\n function buildTreeLines(\n node: Renderable,\n prefix: string = \"\",\n parentPrefix: string = \"\",\n isLastChild: boolean = true,\n depth: number = 0,\n ): string[] {\n if (depth >= maxDepth) {\n return [`${prefix}${node.id} ... (max depth reached)`]\n }\n\n const lines: string[] = []\n const children = node.getChildren()\n\n // Add current node\n lines.push(`${prefix}${node.id}`)\n\n if (children.length > 0) {\n const lastChildIndex = children.length - 1\n\n children.forEach((child, index) => {\n const childIsLast = index === lastChildIndex\n const connector = childIsLast ? \"└── \" : \"├── \"\n const childPrefix = parentPrefix + (isLastChild ? \" \" : \"│ \")\n const childLines = buildTreeLines(child, childPrefix + connector, childPrefix, childIsLast, depth + 1)\n lines.push(...childLines)\n })\n }\n\n return lines\n }\n\n const treeLines = buildTreeLines(renderable)\n console.log(\"Renderable Tree:\\n\" + treeLines.join(\"\\n\"))\n}\n",
15
15
  "import type { TextRenderable } from \"../renderables/Text.js\"\nimport type { TextBuffer, TextChunk } from \"../text-buffer.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport { parseColor, type ColorInput } from \"./RGBA.js\"\n\nconst BrandedStyledText: unique symbol = Symbol.for(\"@fairyhunter13/opentui-core/StyledText\")\n\nexport type Color = ColorInput\n\nexport interface StyleAttrs {\n fg?: Color\n bg?: Color\n bold?: boolean\n italic?: boolean\n underline?: boolean\n strikethrough?: boolean\n dim?: boolean\n reverse?: boolean\n blink?: boolean\n}\n\nexport function isStyledText(obj: any): obj is StyledText {\n return obj && obj[BrandedStyledText]\n}\n\nexport class StyledText {\n [BrandedStyledText] = true\n\n public chunks: TextChunk[]\n\n constructor(chunks: TextChunk[]) {\n this.chunks = chunks\n }\n}\n\nexport function stringToStyledText(content: string): StyledText {\n const chunk = {\n __isChunk: true as const,\n text: content,\n }\n return new StyledText([chunk])\n}\n\nexport type StylableInput = string | number | boolean | TextChunk\n\nfunction applyStyle(input: StylableInput, style: StyleAttrs): TextChunk {\n if (typeof input === \"object\" && \"__isChunk\" in input) {\n const existingChunk = input as TextChunk\n\n const fg = style.fg ? parseColor(style.fg) : existingChunk.fg\n const bg = style.bg ? parseColor(style.bg) : existingChunk.bg\n\n const newAttrs = createTextAttributes(style)\n const mergedAttrs = existingChunk.attributes ? existingChunk.attributes | newAttrs : newAttrs\n\n return {\n __isChunk: true,\n text: existingChunk.text,\n fg,\n bg,\n attributes: mergedAttrs,\n link: existingChunk.link,\n }\n } else {\n const plainTextStr = String(input)\n const fg = style.fg ? parseColor(style.fg) : undefined\n const bg = style.bg ? parseColor(style.bg) : undefined\n const attributes = createTextAttributes(style)\n\n return {\n __isChunk: true,\n text: plainTextStr,\n fg,\n bg,\n attributes,\n }\n }\n}\n\n// Color functions\nexport const black = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"black\" })\nexport const red = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"red\" })\nexport const green = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"green\" })\nexport const yellow = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"yellow\" })\nexport const blue = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"blue\" })\nexport const magenta = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"magenta\" })\nexport const cyan = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"cyan\" })\nexport const white = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"white\" })\n\n// Bright color functions\nexport const brightBlack = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightBlack\" })\nexport const brightRed = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightRed\" })\nexport const brightGreen = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightGreen\" })\nexport const brightYellow = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightYellow\" })\nexport const brightBlue = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightBlue\" })\nexport const brightMagenta = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightMagenta\" })\nexport const brightCyan = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightCyan\" })\nexport const brightWhite = (input: StylableInput): TextChunk => applyStyle(input, { fg: \"brightWhite\" })\n\n// Background color functions\nexport const bgBlack = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"black\" })\nexport const bgRed = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"red\" })\nexport const bgGreen = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"green\" })\nexport const bgYellow = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"yellow\" })\nexport const bgBlue = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"blue\" })\nexport const bgMagenta = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"magenta\" })\nexport const bgCyan = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"cyan\" })\nexport const bgWhite = (input: StylableInput): TextChunk => applyStyle(input, { bg: \"white\" })\n\n// Style functions\nexport const bold = (input: StylableInput): TextChunk => applyStyle(input, { bold: true })\nexport const italic = (input: StylableInput): TextChunk => applyStyle(input, { italic: true })\nexport const underline = (input: StylableInput): TextChunk => applyStyle(input, { underline: true })\nexport const strikethrough = (input: StylableInput): TextChunk => applyStyle(input, { strikethrough: true })\nexport const dim = (input: StylableInput): TextChunk => applyStyle(input, { dim: true })\nexport const reverse = (input: StylableInput): TextChunk => applyStyle(input, { reverse: true })\nexport const blink = (input: StylableInput): TextChunk => applyStyle(input, { blink: true })\n\n// Custom color functions\nexport const fg =\n (color: Color) =>\n (input: StylableInput): TextChunk =>\n applyStyle(input, { fg: color })\nexport const bg =\n (color: Color) =>\n (input: StylableInput): TextChunk =>\n applyStyle(input, { bg: color })\n\nexport const link =\n (url: string) =>\n (input: StylableInput): TextChunk => {\n const chunk =\n typeof input === \"object\" && \"__isChunk\" in input\n ? (input as TextChunk)\n : {\n __isChunk: true as const,\n text: String(input),\n }\n\n return {\n ...chunk,\n link: { url },\n }\n }\n\n/**\n * Template literal handler for styled text (non-cached version).\n * Returns a StyledText object containing chunks of text with optional styles.\n */\nexport function t(strings: TemplateStringsArray, ...values: StylableInput[]): StyledText {\n const chunks: TextChunk[] = []\n\n for (let i = 0; i < strings.length; i++) {\n const raw = strings[i]\n\n if (raw) {\n chunks.push({\n __isChunk: true,\n text: raw,\n attributes: 0,\n })\n }\n\n const val = values[i]\n if (typeof val === \"object\" && \"__isChunk\" in val) {\n chunks.push(val as TextChunk)\n } else if (val !== undefined) {\n const plainTextStr = String(val)\n chunks.push({\n __isChunk: true,\n text: plainTextStr,\n attributes: 0,\n })\n }\n }\n\n return new StyledText(chunks)\n}\n",
@@ -22,13 +22,13 @@
22
22
  "export type MouseEventType = \"down\" | \"up\" | \"move\" | \"drag\" | \"drag-end\" | \"drop\" | \"over\" | \"out\" | \"scroll\"\n\nexport interface ScrollInfo {\n direction: \"up\" | \"down\" | \"left\" | \"right\"\n delta: number\n}\n\nexport type RawMouseEvent = {\n type: MouseEventType\n button: number\n x: number\n y: number\n modifiers: { shift: boolean; alt: boolean; ctrl: boolean }\n scroll?: ScrollInfo\n}\n\ntype ParsedMouseSequence = {\n event: RawMouseEvent\n consumed: number\n}\n\nexport class MouseParser {\n private mouseButtonsPressed = new Set<number>()\n\n private static readonly SCROLL_DIRECTIONS: Record<number, \"up\" | \"down\" | \"left\" | \"right\"> = {\n 0: \"up\",\n 1: \"down\",\n 2: \"left\",\n 3: \"right\",\n }\n\n public reset(): void {\n this.mouseButtonsPressed.clear()\n }\n\n // Preserve raw byte values so X10 payload bytes >= 0x80 remain intact.\n // SGR sequences are ASCII digits + separators and are unaffected either way.\n private decodeInput(data: Buffer | Uint8Array): string {\n const buf = Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength)\n return buf.toString(\"latin1\")\n }\n\n public parseMouseEvent(data: Buffer | Uint8Array): RawMouseEvent | null {\n const str = this.decodeInput(data)\n const parsed = this.parseMouseSequenceAt(str, 0)\n return parsed?.event ?? null\n }\n\n public parseAllMouseEvents(data: Buffer | Uint8Array): RawMouseEvent[] {\n const str = this.decodeInput(data)\n const events: RawMouseEvent[] = []\n let offset = 0\n\n while (offset < str.length) {\n const parsed = this.parseMouseSequenceAt(str, offset)\n if (!parsed) {\n // Stop at the first non-mouse sequence. Callers can decide whether to\n // route any remaining data through keyboard/terminal input handling.\n break\n }\n\n events.push(parsed.event)\n offset += parsed.consumed\n }\n\n return events\n }\n\n private parseMouseSequenceAt(str: string, offset: number): ParsedMouseSequence | null {\n if (!str.startsWith(\"\\x1b[\", offset)) return null\n const introducer = str[offset + 2]\n\n if (introducer === \"<\") {\n return this.parseSgrSequence(str, offset)\n }\n\n if (introducer === \"M\") {\n return this.parseBasicSequence(str, offset)\n }\n\n return null\n }\n\n private parseSgrSequence(str: string, offset: number): ParsedMouseSequence | null {\n let index = offset + 3\n const values = [0, 0, 0]\n let part = 0\n let hasDigit = false\n\n while (index < str.length) {\n const char = str[index]\n const charCode = str.charCodeAt(index)\n\n if (charCode >= 48 && charCode <= 57) {\n hasDigit = true\n values[part] = values[part]! * 10 + (charCode - 48)\n index++\n continue\n }\n\n switch (char) {\n case \";\": {\n if (!hasDigit || part >= 2) return null\n part++\n hasDigit = false\n index++\n break\n }\n case \"M\":\n case \"m\": {\n if (!hasDigit || part !== 2) return null\n\n return {\n event: this.decodeSgrEvent(values[0]!, values[1]!, values[2]!, char),\n consumed: index - offset + 1,\n }\n }\n default:\n return null\n }\n }\n\n return null\n }\n\n private parseBasicSequence(str: string, offset: number): ParsedMouseSequence | null {\n // ESC [ M + 3 bytes\n if (offset + 6 > str.length) return null\n\n const buttonByte = str.charCodeAt(offset + 3) - 32\n // Convert from 1-based to 0-based\n const x = str.charCodeAt(offset + 4) - 33\n const y = str.charCodeAt(offset + 5) - 33\n\n return {\n event: this.decodeBasicEvent(buttonByte, x, y),\n consumed: 6,\n }\n }\n\n private decodeSgrEvent(rawButtonCode: number, wireX: number, wireY: number, pressRelease: \"M\" | \"m\"): RawMouseEvent {\n const button = rawButtonCode & 3\n const isScroll = (rawButtonCode & 64) !== 0\n const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]\n\n const isMotion = (rawButtonCode & 32) !== 0\n const modifiers = {\n shift: (rawButtonCode & 4) !== 0,\n alt: (rawButtonCode & 8) !== 0,\n ctrl: (rawButtonCode & 16) !== 0,\n }\n\n let type: MouseEventType\n let scrollInfo: ScrollInfo | undefined\n\n if (isMotion) {\n const isDragging = this.mouseButtonsPressed.size > 0\n\n if (button === 3) {\n type = \"move\"\n } else if (isDragging) {\n type = \"drag\"\n } else {\n type = \"move\"\n }\n } else if (isScroll && pressRelease === \"M\") {\n type = \"scroll\"\n scrollInfo = {\n direction: scrollDirection!,\n delta: 1,\n }\n } else {\n type = pressRelease === \"M\" ? \"down\" : \"up\"\n\n if (type === \"down\" && button !== 3) {\n this.mouseButtonsPressed.add(button)\n } else if (type === \"up\") {\n this.mouseButtonsPressed.clear()\n }\n }\n\n return {\n type,\n button: button === 3 ? 0 : button,\n x: wireX - 1,\n y: wireY - 1,\n modifiers,\n scroll: scrollInfo,\n }\n }\n\n private decodeBasicEvent(buttonByte: number, x: number, y: number): RawMouseEvent {\n const button = buttonByte & 3\n const isScroll = (buttonByte & 64) !== 0\n const isMotion = (buttonByte & 32) !== 0\n const scrollDirection = !isScroll ? undefined : MouseParser.SCROLL_DIRECTIONS[button]\n\n const modifiers = {\n shift: (buttonByte & 4) !== 0,\n alt: (buttonByte & 8) !== 0,\n ctrl: (buttonByte & 16) !== 0,\n }\n\n let type: MouseEventType\n let actualButton: number\n let scrollInfo: ScrollInfo | undefined\n\n if (isMotion) {\n type = \"move\"\n actualButton = button === 3 ? -1 : button\n } else if (isScroll) {\n type = \"scroll\"\n actualButton = 0\n scrollInfo = {\n direction: scrollDirection!,\n delta: 1,\n }\n } else {\n type = button === 3 ? \"up\" : \"down\"\n actualButton = button === 3 ? 0 : button\n }\n\n return {\n type,\n button: actualButton,\n x,\n y,\n modifiers,\n scroll: scrollInfo,\n }\n }\n}\n",
23
23
  "import { Renderable, type ViewportBounds } from \"../index.js\"\nimport { coordinateToCharacterIndex, fonts } from \"./ascii.font.js\"\n\nclass SelectionAnchor {\n private relativeX: number\n private relativeY: number\n\n constructor(\n private renderable: Renderable,\n absoluteX: number,\n absoluteY: number,\n ) {\n this.relativeX = absoluteX - this.renderable.x\n this.relativeY = absoluteY - this.renderable.y\n }\n\n get x(): number {\n return this.renderable.x + this.relativeX\n }\n\n get y(): number {\n return this.renderable.y + this.relativeY\n }\n}\n\nexport class Selection {\n private _anchor: SelectionAnchor\n private _focus: { x: number; y: number }\n private _selectedRenderables: Renderable[] = []\n private _touchedRenderables: Renderable[] = []\n private _isActive: boolean = true\n private _isDragging: boolean = true\n private _isStart: boolean = false\n\n constructor(anchorRenderable: Renderable, anchor: { x: number; y: number }, focus: { x: number; y: number }) {\n this._anchor = new SelectionAnchor(anchorRenderable, anchor.x, anchor.y)\n this._focus = { ...focus }\n }\n\n get isStart(): boolean {\n return this._isStart\n }\n\n set isStart(value: boolean) {\n this._isStart = value\n }\n\n get anchor(): { x: number; y: number } {\n return { x: this._anchor.x, y: this._anchor.y }\n }\n\n get focus(): { x: number; y: number } {\n return { ...this._focus }\n }\n\n set focus(value: { x: number; y: number }) {\n this._focus = { ...value }\n }\n\n get isActive(): boolean {\n return this._isActive\n }\n\n set isActive(value: boolean) {\n this._isActive = value\n }\n\n get isDragging(): boolean {\n return this._isDragging\n }\n\n set isDragging(value: boolean) {\n this._isDragging = value\n }\n\n get bounds(): ViewportBounds {\n const minX = Math.min(this._anchor.x, this._focus.x)\n const maxX = Math.max(this._anchor.x, this._focus.x)\n const minY = Math.min(this._anchor.y, this._focus.y)\n const maxY = Math.max(this._anchor.y, this._focus.y)\n\n // Selection bounds are inclusive of both anchor and focus\n // A selection from (0,0) to (0,0) covers 1 cell\n // A selection from (0,0) to (5,3) covers cells from (0,0) to (5,3) inclusive\n const width = maxX - minX + 1\n const height = maxY - minY + 1\n\n return {\n x: minX,\n y: minY,\n width,\n height,\n }\n }\n\n updateSelectedRenderables(selectedRenderables: Renderable[]): void {\n this._selectedRenderables = selectedRenderables\n }\n\n get selectedRenderables(): Renderable[] {\n return this._selectedRenderables\n }\n\n updateTouchedRenderables(touchedRenderables: Renderable[]): void {\n this._touchedRenderables = touchedRenderables\n }\n\n get touchedRenderables(): Renderable[] {\n return this._touchedRenderables\n }\n\n getSelectedText(): string {\n const selectedTexts = this._selectedRenderables\n // Sort by reading order: top-to-bottom, then left-to-right\n .sort((a, b) => {\n const aY = a.y\n const bY = b.y\n if (aY !== bY) {\n return aY - bY\n }\n return a.x - b.x\n })\n .filter((renderable) => !renderable.isDestroyed)\n .map((renderable) => renderable.getSelectedText())\n .filter((text) => text)\n return selectedTexts.join(\"\\n\")\n }\n}\n\nexport interface LocalSelectionBounds {\n anchorX: number\n anchorY: number\n focusX: number\n focusY: number\n isActive: boolean\n}\n\nexport function convertGlobalToLocalSelection(\n globalSelection: Selection | null,\n localX: number,\n localY: number,\n): LocalSelectionBounds | null {\n if (!globalSelection?.isActive) {\n return null\n }\n\n return {\n anchorX: globalSelection.anchor.x - localX,\n anchorY: globalSelection.anchor.y - localY,\n focusX: globalSelection.focus.x - localX,\n focusY: globalSelection.focus.y - localY,\n isActive: true,\n }\n}\n\nexport class ASCIIFontSelectionHelper {\n private localSelection: { start: number; end: number } | null = null\n\n constructor(\n private getText: () => string,\n private getFont: () => keyof typeof fonts,\n ) {}\n\n hasSelection(): boolean {\n return this.localSelection !== null\n }\n\n getSelection(): { start: number; end: number } | null {\n return this.localSelection\n }\n\n shouldStartSelection(localX: number, localY: number, width: number, height: number): boolean {\n if (localX < 0 || localX >= width || localY < 0 || localY >= height) {\n return false\n }\n\n const text = this.getText()\n const font = this.getFont()\n const charIndex = coordinateToCharacterIndex(localX, text, font)\n\n return charIndex >= 0 && charIndex <= text.length\n }\n\n onLocalSelectionChanged(localSelection: LocalSelectionBounds | null, width: number, height: number): boolean {\n const previousSelection = this.localSelection\n\n if (!localSelection?.isActive) {\n this.localSelection = null\n return previousSelection !== null\n }\n\n const text = this.getText()\n const font = this.getFont()\n\n const selStart = { x: localSelection.anchorX, y: localSelection.anchorY }\n const selEnd = { x: localSelection.focusX, y: localSelection.focusY }\n\n if (height - 1 < selStart.y || 0 > selEnd.y) {\n this.localSelection = null\n return previousSelection !== null\n }\n\n let startCharIndex = 0\n let endCharIndex = text.length\n\n if (selStart.y > height - 1) {\n // Selection starts below us - we're not selected\n this.localSelection = null\n return previousSelection !== null\n } else if (selStart.y >= 0 && selStart.y <= height - 1) {\n // Selection starts within our Y range - use the actual start X coordinate\n if (selStart.x > 0) {\n startCharIndex = coordinateToCharacterIndex(selStart.x, text, font)\n }\n }\n\n if (selEnd.y < 0) {\n // Selection ends above us - we're not selected\n this.localSelection = null\n return previousSelection !== null\n } else if (selEnd.y >= 0 && selEnd.y <= height - 1) {\n // Selection ends within our Y range - use the actual end X coordinate\n if (selEnd.x >= 0) {\n endCharIndex = coordinateToCharacterIndex(selEnd.x, text, font)\n } else {\n endCharIndex = 0\n }\n }\n\n if (startCharIndex < endCharIndex && startCharIndex >= 0 && endCharIndex <= text.length) {\n this.localSelection = { start: startCharIndex, end: endCharIndex }\n } else {\n this.localSelection = null\n }\n\n return (\n previousSelection?.start !== this.localSelection?.start || previousSelection?.end !== this.localSelection?.end\n )\n }\n}\n",
24
24
  "const singletonCacheSymbol = Symbol.for(\"@fairyhunter13/opentui-core/singleton\")\n\n/**\n * Ensures a value is initialized once per process,\n * persists across Bun hot reloads, and is type-safe.\n */\nexport function singleton<T>(key: string, factory: () => T): T {\n // @ts-expect-error this symbol is only used in this file and is not part of the public API\n const bag = (globalThis[singletonCacheSymbol] ??= {})\n if (!(key in bag)) {\n bag[key] = factory()\n }\n return bag[key] as T\n}\n\nexport function destroySingleton(key: string): void {\n // @ts-expect-error this symbol is only used in this file and is not part of the public API\n const bag = globalThis[singletonCacheSymbol]\n if (bag && key in bag) {\n delete bag[key]\n }\n}\n\nexport function hasSingleton(key: string): boolean {\n // @ts-expect-error this symbol is only used in this file and is not part of the public API\n const bag = globalThis[singletonCacheSymbol]\n return bag && key in bag\n}\n",
25
- "import { singleton } from \"./singleton.ts\"\n\n/**\n * Environment variable registry\n *\n * Usage:\n * ```ts\n * import { registerEnvVar, env } from \"./lib/env.ts\";\n *\n * // Register environment variables\n * registerEnvVar({\n * name: \"DEBUG\",\n * description: \"Enable debug logging\",\n * type: \"boolean\",\n * default: false\n * });\n *\n * registerEnvVar({\n * name: \"PORT\",\n * description: \"Server port number\",\n * type: \"number\",\n * default: 3000\n * });\n *\n * // Access environment variables\n * if (env.DEBUG) {\n * console.log(\"Debug mode enabled\");\n * }\n *\n * const port = env.PORT; // number\n * ```\n */\n\nexport interface EnvVarConfig {\n name: string\n description: string\n default?: string | boolean | number\n type?: \"string\" | \"boolean\" | \"number\"\n}\n\nexport const envRegistry: Record<string, EnvVarConfig> = singleton(\"env-registry\", () => ({}))\n\nexport function registerEnvVar(config: EnvVarConfig): void {\n const existing = envRegistry[config.name]\n if (existing) {\n if (\n existing.description !== config.description ||\n existing.type !== config.type ||\n existing.default !== config.default\n ) {\n throw new Error(\n `Environment variable \"${config.name}\" is already registered with different configuration. ` +\n `Existing: ${JSON.stringify(existing)}, New: ${JSON.stringify(config)}`,\n )\n }\n return\n }\n envRegistry[config.name] = config\n}\n\nfunction normalizeBoolean(value: string): boolean {\n const lowerValue = value.toLowerCase()\n return [\"true\", \"1\", \"on\", \"yes\"].includes(lowerValue)\n}\n\nfunction parseEnvValue(config: EnvVarConfig): string | boolean | number {\n const envValue = process.env[config.name]\n\n if (envValue === undefined && config.default !== undefined) {\n return config.default\n }\n\n if (envValue === undefined) {\n throw new Error(`Required environment variable ${config.name} is not set. ${config.description}`)\n }\n\n switch (config.type) {\n case \"boolean\":\n return typeof envValue === \"boolean\" ? envValue : normalizeBoolean(envValue)\n case \"number\":\n const numValue = Number(envValue)\n if (isNaN(numValue)) {\n throw new Error(`Environment variable ${config.name} must be a valid number, got: ${envValue}`)\n }\n return numValue\n case \"string\":\n default:\n return envValue\n }\n}\n\nclass EnvStore {\n private parsedValues: Map<string, string | boolean | number> = new Map()\n\n get(key: string): any {\n if (this.parsedValues.has(key)) {\n return this.parsedValues.get(key)!\n }\n\n if (!(key in envRegistry)) {\n throw new Error(`Environment variable ${key} is not registered.`)\n }\n\n try {\n const value = parseEnvValue(envRegistry[key])\n this.parsedValues.set(key, value)\n return value\n } catch (error) {\n throw new Error(`Failed to parse env var ${key}: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n\n has(key: string): boolean {\n return key in envRegistry\n }\n\n clearCache(): void {\n this.parsedValues.clear()\n }\n}\n\nconst envStore = singleton(\"env-store\", () => new EnvStore())\n\nexport function clearEnvCache(): void {\n envStore.clearCache()\n}\n\nexport function generateEnvMarkdown(): string {\n const configs = Object.values(envRegistry)\n\n if (configs.length === 0) {\n return \"# Environment Variables\\n\\nNo environment variables registered.\\n\"\n }\n\n let markdown = \"# Environment Variables\\n\\n\"\n\n for (const config of configs) {\n markdown += `## ${config.name}\\n\\n`\n markdown += `${config.description}\\n\\n`\n\n markdown += `**Type:** \\`${config.type || \"string\"}\\` \\n`\n\n if (config.default !== undefined) {\n const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n markdown += `**Default:** \\`${defaultValue}\\`\\n`\n } else {\n markdown += \"**Default:** *Required*\\n\"\n }\n\n markdown += \"\\n\"\n }\n\n return markdown\n}\n\nexport function generateEnvColored(): string {\n const configs = Object.values(envRegistry)\n\n if (configs.length === 0) {\n return \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\nNo environment variables registered.\\n\"\n }\n\n let output = \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\n\"\n\n for (const config of configs) {\n output += `\\x1b[1;33m${config.name}\\x1b[0m\\n`\n output += `${config.description}\\n`\n output += `\\x1b[32mType:\\x1b[0m \\x1b[36m${config.type || \"string\"}\\x1b[0m\\n`\n\n if (config.default !== undefined) {\n const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n output += `\\x1b[32mDefault:\\x1b[0m \\x1b[35m${defaultValue}\\x1b[0m\\n`\n } else {\n output += `\\x1b[32mDefault:\\x1b[0m \\x1b[31mRequired\\x1b[0m\\n`\n }\n\n output += \"\\n\"\n }\n\n return output\n}\n\nexport const env = new Proxy({} as Record<string, any>, {\n get(target, prop: string) {\n if (typeof prop !== \"string\") {\n return undefined\n }\n return envStore.get(prop)\n },\n\n has(target, prop: string) {\n return envStore.has(prop)\n },\n\n ownKeys() {\n return Object.keys(envRegistry)\n },\n\n getOwnPropertyDescriptor(target, prop: string) {\n if (envStore.has(prop)) {\n return {\n enumerable: true,\n configurable: true,\n get: () => envStore.get(prop),\n }\n }\n return undefined\n },\n})\n",
26
- "// Byte-level stdin parser that turns raw terminal input into typed StdinEvents.\n//\n// This replaces a two-phase token -> decode pipeline with a single state machine\n// that produces fully typed events (key, mouse, paste, response) directly from\n// bytes. The parser owns all byte framing and protocol recognition. It does NOT\n// own event dispatch — that belongs to KeyHandler and the renderer.\n\nimport { Buffer } from \"node:buffer\"\nimport { SystemClock, type Clock, type TimerHandle } from \"./clock\"\nimport { parseKeypress, type ParsedKey } from \"./parse.keypress\"\nimport { MouseParser, type RawMouseEvent } from \"./parse.mouse\"\nimport type { PasteMetadata } from \"./paste\"\n\nexport { SystemClock, type Clock, type TimerHandle } from \"./clock\"\n\nexport type StdinResponseProtocol = \"csi\" | \"osc\" | \"dcs\" | \"apc\" | \"unknown\"\n\n// The four event types the parser produces. Everything stdin sends becomes\n// exactly one of these.\nexport type StdinEvent =\n | {\n type: \"key\"\n raw: string\n key: ParsedKey\n }\n | {\n type: \"mouse\"\n raw: string\n encoding: \"sgr\" | \"x10\"\n event: RawMouseEvent\n }\n | {\n type: \"paste\"\n bytes: Uint8Array\n metadata?: PasteMetadata\n }\n | {\n type: \"response\"\n protocol: StdinResponseProtocol\n sequence: string\n }\n\nexport interface StdinParserProtocolContext {\n kittyKeyboardEnabled: boolean\n privateCapabilityRepliesActive: boolean\n pixelResolutionQueryActive: boolean\n explicitWidthCprActive: boolean\n}\n\nexport interface StdinParserOptions {\n timeoutMs?: number\n maxPendingBytes?: number\n armTimeouts?: boolean\n onTimeoutFlush?: () => void\n useKittyKeyboard?: boolean\n protocolContext?: Partial<StdinParserProtocolContext>\n clock?: Clock\n}\n\n// State machine tags for the byte scanner. Each tag represents which protocol\n// framing mode the parser is currently inside. The sawEsc flag in osc/dcs/apc\n// tracks whether the previous byte was ESC, since the two-byte ST terminator\n// (ESC \\) can split across push() calls.\ntype ParserState =\n | { tag: \"ground\" }\n | { tag: \"utf8\"; expected: number; seen: number }\n | { tag: \"esc\" }\n | { tag: \"ss3\" }\n | { tag: \"csi\" }\n | { tag: \"csi_sgr_mouse\"; part: number; hasDigit: boolean }\n | { tag: \"csi_sgr_mouse_deferred\"; part: number; hasDigit: boolean }\n | { tag: \"csi_parametric\"; semicolons: number; segments: number; hasDigit: boolean; firstParamValue: number | null }\n | {\n tag: \"csi_parametric_deferred\"\n semicolons: number\n segments: number\n hasDigit: boolean\n firstParamValue: number | null\n }\n | { tag: \"csi_private_reply\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n | { tag: \"csi_private_reply_deferred\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n | { tag: \"osc\"; sawEsc: boolean }\n | { tag: \"dcs\"; sawEsc: boolean }\n | { tag: \"apc\"; sawEsc: boolean }\n | { tag: \"esc_recovery\" }\n | { tag: \"esc_less_mouse\" }\n | { tag: \"esc_less_x10_mouse\" }\n\n// Collects paste body incrementally, bypassing the main ByteQueue so large\n// pastes don't grow the parser buffer. Keeps only a small tail for end-marker\n// detection across chunk boundaries.\ninterface PasteCollector {\n tail: Uint8Array\n parts: Uint8Array[]\n totalLength: number\n}\n\n// 10ms is enough to distinguish a lone ESC keypress from the start of an\n// escape sequence on all but the slowest connections.\nconst DEFAULT_TIMEOUT_MS = 10\nconst DEFAULT_MAX_PENDING_BYTES = 64 * 1024\nconst INITIAL_PENDING_CAPACITY = 256\nconst ESC = 0x1b\nconst BEL = 0x07\nconst BRACKETED_PASTE_START = Buffer.from(\"\\x1b[200~\")\nconst BRACKETED_PASTE_END = Buffer.from(\"\\x1b[201~\")\nconst EMPTY_BYTES = new Uint8Array(0)\nconst KEY_DECODER = new TextDecoder()\nconst DEFAULT_PROTOCOL_CONTEXT: StdinParserProtocolContext = {\n kittyKeyboardEnabled: false,\n privateCapabilityRepliesActive: false,\n pixelResolutionQueryActive: false,\n explicitWidthCprActive: false,\n}\n// rxvt uses $-terminated CSI sequences for shifted function keys (e.g. ESC[2$).\n// Standard CSI treats $ as an intermediate byte, not a final, so we match these\n// explicitly to avoid waiting for a \"real\" final byte that never arrives.\nconst RXVT_DOLLAR_CSI_RE = /^\\x1b\\[\\d+\\$$/\n\nconst SYSTEM_CLOCK = new SystemClock()\n\n// Byte buffer for pending input. Uses start/end offsets so consume() just\n// advances the start pointer without copying. Compacts (via copyWithin) only\n// when the consumed prefix exceeds half the buffer, keeping amortized cost low.\nclass ByteQueue {\n private buf: Uint8Array\n private start = 0\n private end = 0\n\n constructor(capacity = INITIAL_PENDING_CAPACITY) {\n this.buf = new Uint8Array(capacity)\n }\n\n get length(): number {\n return this.end - this.start\n }\n\n get capacity(): number {\n return this.buf.length\n }\n\n view(): Uint8Array {\n return this.buf.subarray(this.start, this.end)\n }\n\n // Returns a view of the contents and resets the queue. The view shares\n // the underlying buffer, so it becomes invalid on the next append().\n take(): Uint8Array {\n const chunk = this.view()\n this.start = 0\n this.end = 0\n return chunk\n }\n\n append(chunk: Uint8Array): void {\n if (chunk.length === 0) {\n return\n }\n\n this.ensureCapacity(this.length + chunk.length)\n this.buf.set(chunk, this.end)\n this.end += chunk.length\n }\n\n // Drops the first `count` bytes. Compacts when the consumed prefix\n // exceeds half the buffer to reclaim wasted space at the front.\n consume(count: number): void {\n if (count <= 0) {\n return\n }\n\n if (count >= this.length) {\n this.start = 0\n this.end = 0\n return\n }\n\n this.start += count\n if (this.start >= this.buf.length / 2) {\n this.buf.copyWithin(0, this.start, this.end)\n this.end -= this.start\n this.start = 0\n }\n }\n\n clear(): void {\n this.start = 0\n this.end = 0\n }\n\n reset(capacity = INITIAL_PENDING_CAPACITY): void {\n this.buf = new Uint8Array(capacity)\n this.start = 0\n this.end = 0\n }\n\n // Tries reclaiming space by compacting data to the front first.\n // Doubles the allocation if that still isn't enough.\n private ensureCapacity(requiredLength: number): void {\n const currentLength = this.length\n if (requiredLength <= this.buf.length) {\n const availableAtEnd = this.buf.length - this.end\n if (availableAtEnd >= requiredLength - currentLength) {\n return\n }\n\n this.buf.copyWithin(0, this.start, this.end)\n this.end = currentLength\n this.start = 0\n if (requiredLength <= this.buf.length) {\n return\n }\n }\n\n let nextCapacity = this.buf.length\n while (nextCapacity < requiredLength) {\n nextCapacity *= 2\n }\n\n const next = new Uint8Array(nextCapacity)\n next.set(this.view(), 0)\n this.buf = next\n this.start = 0\n this.end = currentLength\n }\n}\n\nfunction normalizePositiveOption(value: number | undefined, fallback: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n return fallback\n }\n\n return Math.floor(value)\n}\n\n// Returns the expected byte count for a UTF-8 sequence given its lead byte,\n// or 0 for bytes that aren't valid UTF-8 leads. Returning 0 tells the parser\n// this is a legacy high-byte character (0x80–0xBF, 0xC0–0xC1, 0xF5+) that\n// goes through the parseKeypress() meta-key path instead.\nfunction utf8SequenceLength(first: number): number {\n if (first < 0x80) return 1\n if (first >= 0xc2 && first <= 0xdf) return 2\n if (first >= 0xe0 && first <= 0xef) return 3\n if (first >= 0xf0 && first <= 0xf4) return 4\n return 0\n}\n\nfunction bytesEqual(left: Uint8Array, right: Uint8Array): boolean {\n if (left.length !== right.length) {\n return false\n }\n\n for (let index = 0; index < left.length; index += 1) {\n if (left[index] !== right[index]) {\n return false\n }\n }\n\n return true\n}\n\n// Checks whether a byte sequence is a complete SGR mouse report:\n// ESC [ < Ps ; Ps ; Ps M/m (three semicolon-separated digit groups).\nfunction isMouseSgrSequence(sequence: Uint8Array): boolean {\n if (sequence.length < 7) {\n return false\n }\n\n if (sequence[0] !== ESC || sequence[1] !== 0x5b || sequence[2] !== 0x3c) {\n return false\n }\n\n const final = sequence[sequence.length - 1]\n if (final !== 0x4d && final !== 0x6d) {\n return false\n }\n\n let part = 0\n let hasDigit = false\n for (let index = 3; index < sequence.length - 1; index += 1) {\n const byte = sequence[index]!\n if (byte >= 0x30 && byte <= 0x39) {\n hasDigit = true\n continue\n }\n\n if (byte === 0x3b && hasDigit && part < 2) {\n part += 1\n hasDigit = false\n continue\n }\n\n return false\n }\n\n return part === 2 && hasDigit\n}\n\nfunction isAsciiDigit(byte: number): boolean {\n return byte >= 0x30 && byte <= 0x39\n}\n\ninterface ParametricCsiLike {\n semicolons: number\n segments: number\n hasDigit: boolean\n firstParamValue: number | null\n}\n\ninterface PrivateReplyCsiLike {\n semicolons: number\n hasDigit: boolean\n sawDollar: boolean\n}\n\nfunction parsePositiveDecimalPrefix(sequence: Uint8Array, start: number, endExclusive: number): number | null {\n if (start >= endExclusive) return null\n\n let value = 0\n let sawDigit = false\n for (let index = start; index < endExclusive; index += 1) {\n const byte = sequence[index]!\n if (!isAsciiDigit(byte)) return null\n sawDigit = true\n value = value * 10 + (byte - 0x30)\n }\n\n return sawDigit ? value : null\n}\n\nfunction canStillBeKittyU(state: ParametricCsiLike): boolean {\n return state.semicolons >= 1\n}\n\nfunction canStillBeKittySpecial(state: ParametricCsiLike): boolean {\n return state.semicolons === 1 && state.segments > 1\n}\n\nfunction canStillBeExplicitWidthCpr(state: ParametricCsiLike): boolean {\n return state.firstParamValue === 1 && state.semicolons === 1\n}\n\nfunction canStillBePixelResolution(state: ParametricCsiLike): boolean {\n return state.firstParamValue === 4 && state.semicolons === 2\n}\n\nfunction canDeferParametricCsi(state: ParametricCsiLike, context: StdinParserProtocolContext): boolean {\n return (\n (context.kittyKeyboardEnabled && (canStillBeKittyU(state) || canStillBeKittySpecial(state))) ||\n (context.explicitWidthCprActive && canStillBeExplicitWidthCpr(state)) ||\n (context.pixelResolutionQueryActive && canStillBePixelResolution(state))\n )\n}\n\nfunction canCompleteDeferredParametricCsi(\n state: ParametricCsiLike,\n byte: number,\n context: StdinParserProtocolContext,\n): boolean {\n if (context.kittyKeyboardEnabled) {\n if (state.hasDigit && byte === 0x75) return true\n if (\n state.hasDigit &&\n state.semicolons === 1 &&\n state.segments > 1 &&\n (byte === 0x7e || (byte >= 0x41 && byte <= 0x5a))\n ) {\n return true\n }\n }\n\n if (\n context.explicitWidthCprActive &&\n state.hasDigit &&\n state.firstParamValue === 1 &&\n state.semicolons === 1 &&\n byte === 0x52\n ) {\n return true\n }\n\n if (\n context.pixelResolutionQueryActive &&\n state.hasDigit &&\n state.firstParamValue === 4 &&\n state.semicolons === 2 &&\n byte === 0x74\n ) {\n return true\n }\n\n return false\n}\n\nfunction canDeferPrivateReplyCsi(context: StdinParserProtocolContext): boolean {\n return context.privateCapabilityRepliesActive\n}\n\nfunction canCompleteDeferredPrivateReplyCsi(\n state: PrivateReplyCsiLike,\n byte: number,\n context: StdinParserProtocolContext,\n): boolean {\n if (!context.privateCapabilityRepliesActive) return false\n if (state.sawDollar) return state.hasDigit && byte === 0x79\n if (byte === 0x63) return state.hasDigit || state.semicolons > 0\n return state.hasDigit && byte === 0x75\n}\n\nfunction concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {\n if (left.length === 0) {\n return right\n }\n\n if (right.length === 0) {\n return left\n }\n\n const combined = new Uint8Array(left.length + right.length)\n combined.set(left, 0)\n combined.set(right, left.length)\n return combined\n}\n\nfunction indexOfBytes(haystack: Uint8Array, needle: Uint8Array): number {\n if (needle.length === 0) {\n return 0\n }\n\n const limit = haystack.length - needle.length\n for (let offset = 0; offset <= limit; offset += 1) {\n let matched = true\n for (let index = 0; index < needle.length; index += 1) {\n if (haystack[offset + index] !== needle[index]) {\n matched = false\n break\n }\n }\n\n if (matched) {\n return offset\n }\n }\n\n return -1\n}\n\n// Decodes raw protocol bytes as latin1. Used for mouse and response events\n// where the wire bytes may not be valid UTF-8 but need a lossless string\n// form for downstream sequence handlers.\nfunction decodeLatin1(bytes: Uint8Array): string {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(\"latin1\")\n}\n\nfunction decodeUtf8(bytes: Uint8Array): string {\n return KEY_DECODER.decode(bytes)\n}\n\nfunction createPasteCollector(): PasteCollector {\n return {\n tail: EMPTY_BYTES,\n parts: [],\n totalLength: 0,\n }\n}\n\nfunction joinPasteBytes(parts: Uint8Array[], totalLength: number): Uint8Array {\n if (totalLength === 0) {\n return EMPTY_BYTES\n }\n\n if (parts.length === 1) {\n return parts[0]!\n }\n\n const bytes = new Uint8Array(totalLength)\n let offset = 0\n for (const part of parts) {\n bytes.set(part, offset)\n offset += part.length\n }\n\n return bytes\n}\n\n// Push-driven stdin parser. Callers feed raw bytes via push(), then read\n// typed events via read() or drain(). At most one incomplete protocol unit\n// is buffered at a time; everything else is immediately converted to events.\n//\n// The parser guarantees chunk-shape invariance: the same bytes always produce\n// the same events, regardless of chunk boundaries. A lone ESC resolves via\n// timeout, split UTF-8 codepoints reassemble correctly, and bracketed paste\n// markers may split across any chunk boundary.\nexport class StdinParser {\n private readonly pending = new ByteQueue(INITIAL_PENDING_CAPACITY)\n private readonly events: StdinEvent[] = []\n private readonly timeoutMs: number\n private readonly maxPendingBytes: number\n private readonly armTimeouts: boolean\n private readonly onTimeoutFlush: (() => void) | null\n private readonly useKittyKeyboard: boolean\n private readonly mouseParser = new MouseParser()\n private readonly clock: Clock\n private protocolContext: StdinParserProtocolContext\n private timeoutId: TimerHandle | null = null\n private destroyed = false\n // When the current incomplete unit first appeared. Null when nothing is pending.\n private pendingSinceMs: number | null = null\n // When true, the state machine treats the current incomplete prefix as\n // final and emits it as one atomic event (e.g. a lone ESC becomes an\n // Escape key). Set by the timeout, consumed by the next read() or drain().\n private forceFlush = false\n // True only immediately after a timeout flush emits a lone ESC key. The next\n // `[` may begin a delayed `[<...M/m` mouse continuation recovery path.\n private justFlushedEsc = false\n private state: ParserState = { tag: \"ground\" }\n // Scan position within pending.view() during scanPending().\n private cursor = 0\n // Start of the protocol unit currently being parsed. The bytes from\n // unitStart through cursor all belong to one atomic unit.\n private unitStart = 0\n // When non-null, the parser is inside a bracketed paste. All incoming\n // bytes flow through consumePasteBytes() instead of the normal state machine.\n private paste: PasteCollector | null = null\n\n constructor(options: StdinParserOptions = {}) {\n this.timeoutMs = normalizePositiveOption(options.timeoutMs, DEFAULT_TIMEOUT_MS)\n this.maxPendingBytes = normalizePositiveOption(options.maxPendingBytes, DEFAULT_MAX_PENDING_BYTES)\n this.armTimeouts = options.armTimeouts ?? true\n this.onTimeoutFlush = options.onTimeoutFlush ?? null\n this.useKittyKeyboard = options.useKittyKeyboard ?? true\n this.clock = options.clock ?? SYSTEM_CLOCK\n this.protocolContext = {\n ...DEFAULT_PROTOCOL_CONTEXT,\n kittyKeyboardEnabled: options.protocolContext?.kittyKeyboardEnabled ?? false,\n privateCapabilityRepliesActive: options.protocolContext?.privateCapabilityRepliesActive ?? false,\n pixelResolutionQueryActive: options.protocolContext?.pixelResolutionQueryActive ?? false,\n explicitWidthCprActive: options.protocolContext?.explicitWidthCprActive ?? false,\n }\n }\n\n public get bufferCapacity(): number {\n return this.pending.capacity\n }\n\n public updateProtocolContext(patch: Partial<StdinParserProtocolContext>): void {\n this.ensureAlive()\n this.protocolContext = { ...this.protocolContext, ...patch }\n this.reconcileDeferredStateWithProtocolContext()\n this.reconcileTimeoutState()\n }\n\n // Feeds raw stdin bytes into the parser. Converts as much as possible into\n // queued events and leaves at most one incomplete unit behind in pending.\n //\n // When a chunk contains a paste start marker, bytes before the marker go\n // through normal parsing, then paste mode takes over for the rest. This\n // prevents large pastes from growing the main buffer.\n public push(data: Uint8Array): void {\n this.ensureAlive()\n if (data.length === 0) {\n // Preserve the existing empty-chunk -> empty-keypress behavior.\n this.emitKeyOrResponse(\"unknown\", \"\")\n return\n }\n\n let remainder = data\n while (remainder.length > 0) {\n if (this.paste) {\n remainder = this.consumePasteBytes(remainder)\n continue\n }\n\n // If we're in ground state with nothing pending, scan the incoming\n // chunk for a paste start marker. Only append through the marker so\n // scanPending() enters paste mode without buffering the full paste.\n const immediatePasteStartIndex =\n this.state.tag === \"ground\" && this.pending.length === 0 ? indexOfBytes(remainder, BRACKETED_PASTE_START) : -1\n const appendEnd =\n immediatePasteStartIndex === -1 ? remainder.length : immediatePasteStartIndex + BRACKETED_PASTE_START.length\n\n this.pending.append(remainder.subarray(0, appendEnd))\n remainder = remainder.subarray(appendEnd)\n this.scanPending()\n\n if (this.paste && this.pending.length > 0) {\n remainder = this.consumePasteBytes(this.takePendingBytes())\n continue\n }\n\n if (!this.paste && this.pending.length > this.maxPendingBytes) {\n this.flushPendingOverflow()\n this.scanPending()\n\n if (this.paste && this.pending.length > 0) {\n remainder = this.consumePasteBytes(this.takePendingBytes())\n }\n }\n }\n\n this.reconcileTimeoutState()\n }\n\n // Pops one event from the queue. If the queue is empty and a timeout has\n // set forceFlush, re-scans pending to convert the timed-out incomplete\n // unit into one final event before returning it.\n public read(): StdinEvent | null {\n this.ensureAlive()\n\n if (this.events.length === 0 && this.forceFlush) {\n this.scanPending()\n this.reconcileTimeoutState()\n }\n\n return this.events.shift() ?? null\n }\n\n // Delivers all queued events. Stops early if the parser is destroyed\n // during a callback (e.g. an event handler triggers teardown).\n public drain(onEvent: (event: StdinEvent) => void): void {\n this.ensureAlive()\n\n while (true) {\n if (this.destroyed) {\n return\n }\n\n const event = this.read()\n if (!event) {\n return\n }\n\n onEvent(event)\n }\n }\n\n // Marks the parser for forced flush if enough time has passed since\n // incomplete data arrived. Does not immediately emit events — the next\n // read() or drain() does the actual flush. This separation keeps the\n // timer callback from emitting events mid-flight in user code.\n public flushTimeout(nowMsValue: number = this.clock.now()): void {\n this.ensureAlive()\n\n if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n return\n }\n\n if (nowMsValue < this.pendingSinceMs || nowMsValue - this.pendingSinceMs < this.timeoutMs) {\n return\n }\n\n this.forceFlush = true\n }\n\n public reset(): void {\n if (this.destroyed) {\n return\n }\n\n this.clearTimeout()\n this.resetState()\n }\n\n public resetMouseState(): void {\n this.ensureAlive()\n this.mouseParser.reset()\n }\n\n public destroy(): void {\n if (this.destroyed) {\n return\n }\n\n this.clearTimeout()\n this.destroyed = true\n this.resetState()\n }\n\n private ensureAlive(): void {\n if (this.destroyed) {\n throw new Error(\"StdinParser has been destroyed\")\n }\n }\n\n // Scans the pending byte buffer one byte at a time, dispatching on the\n // current parser state. All protocol framing lives in this single switch\n // — intentionally not split into per-mode scan helpers.\n //\n // Exits when: all bytes consumed (ground), more bytes needed (incomplete\n // unit), or paste mode entered (body handled by consumePasteBytes).\n private scanPending(): void {\n while (!this.paste) {\n const bytes = this.pending.view()\n if (this.state.tag === \"ground\" && this.cursor >= bytes.length) {\n this.pending.clear()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n const byte = this.cursor < bytes.length ? bytes[this.cursor]! : -1\n switch (this.state.tag) {\n case \"ground\": {\n this.unitStart = this.cursor\n\n // After a timeout-flushed lone ESC, a following `[` may be the start\n // of a delayed `[<...M/m` mouse continuation. Recover only this narrow\n // case; otherwise clear the recovery flag and parse bytes normally.\n if (this.justFlushedEsc) {\n if (byte === 0x5b) {\n this.justFlushedEsc = false\n this.cursor += 1\n this.state = { tag: \"esc_recovery\" }\n continue\n }\n\n this.justFlushedEsc = false\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"esc\" }\n continue\n }\n\n if (byte < 0x80) {\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.cursor, this.cursor + 1)))\n this.consumePrefix(this.cursor + 1)\n continue\n }\n\n // Invalid UTF-8 lead byte. Could be a legacy high-byte from an\n // older terminal. If it's the last byte in the buffer, wait for\n // more data or a timeout before committing. On timeout, emit\n // through parseKeypress() which handles meta-key behavior.\n const expected = utf8SequenceLength(byte)\n if (expected === 0) {\n if (!this.forceFlush && this.cursor + 1 === bytes.length) {\n this.markPending()\n return\n }\n\n this.emitLegacyHighByte(byte)\n this.consumePrefix(this.cursor + 1)\n continue\n }\n\n this.cursor += 1\n this.state = { tag: \"utf8\", expected, seen: 1 }\n continue\n }\n\n case \"utf8\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitLegacyHighByte(bytes[this.unitStart]!)\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n // Not a valid continuation byte. Treat the lead byte as a legacy\n // high-byte character and restart parsing from this position.\n if ((byte & 0xc0) !== 0x80) {\n this.emitLegacyHighByte(bytes[this.unitStart]!)\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n const nextSeen = this.state.seen + 1\n this.cursor += 1\n if (nextSeen < this.state.expected) {\n this.state = { tag: \"utf8\", expected: this.state.expected, seen: nextSeen }\n continue\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"esc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n const flushedLoneEsc = this.cursor === this.unitStart + 1 && bytes[this.unitStart] === ESC\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.justFlushedEsc = flushedLoneEsc\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // The byte after ESC determines the sub-protocol:\n // [ -> CSI, O -> SS3, ] -> OSC, P -> DCS, _ -> APC.\n switch (byte) {\n case 0x5b:\n this.cursor += 1\n this.state = { tag: \"csi\" }\n continue\n case 0x4f:\n this.cursor += 1\n this.state = { tag: \"ss3\" }\n continue\n case 0x5d:\n this.cursor += 1\n this.state = { tag: \"osc\", sawEsc: false }\n continue\n case 0x50:\n this.cursor += 1\n this.state = { tag: \"dcs\", sawEsc: false }\n continue\n case 0x5f:\n this.cursor += 1\n this.state = { tag: \"apc\", sawEsc: false }\n continue\n // ESC ESC: stay in esc state. Terminals encode Alt+ESC and\n // similar sequences as ESC ESC [...], so we keep scanning.\n case ESC:\n this.cursor += 1\n continue\n default:\n this.cursor += 1\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n }\n\n case \"ss3\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n this.cursor += 1\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // Narrow recovery path for delayed mouse continuations after a\n // timeout-flushed lone ESC. Wait for either `<` (SGR) or `M` (X10); if\n // neither arrives, flush `[` as a normal key.\n case \"esc_recovery\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === 0x3c) {\n this.cursor += 1\n this.state = { tag: \"esc_less_mouse\" }\n continue\n }\n\n if (byte === 0x4d) {\n this.cursor += 1\n this.state = { tag: \"esc_less_x10_mouse\" }\n continue\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.unitStart + 1)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n case \"csi\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // A new ESC inside an incomplete CSI means the previous sequence\n // was interrupted. Flush everything before the new ESC as one\n // opaque response, then restart parsing at the new ESC.\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // X10 mouse: ESC [ M plus 3 raw payload bytes (button, x, y).\n // cursor === unitStart + 2 confirms M comes right after ESC[,\n // not as a later final byte in a different CSI sequence.\n if (byte === 0x4d && this.cursor === this.unitStart + 2) {\n const end = this.cursor + 4\n if (bytes.length < end) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n this.state = { tag: \"ground\" }\n this.consumePrefix(bytes.length)\n continue\n }\n\n this.emitMouse(bytes.subarray(this.unitStart, end), \"x10\")\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n if (byte === 0x24) {\n const candidateEnd = this.cursor + 1\n const candidate = decodeUtf8(bytes.subarray(this.unitStart, candidateEnd))\n if (RXVT_DOLLAR_CSI_RE.test(candidate)) {\n this.emitKeyOrResponse(\"csi\", candidate)\n this.state = { tag: \"ground\" }\n this.consumePrefix(candidateEnd)\n continue\n }\n\n if (!this.forceFlush && candidateEnd >= bytes.length) {\n this.markPending()\n return\n }\n }\n\n if (byte === 0x3c && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: 0, hasDigit: false }\n continue\n }\n\n // Some terminals use ESC [[A..E / ESC [[5~ / ESC [[6~ variants.\n // Treat the second `[` immediately after ESC[ as part of the CSI\n // payload instead of as a final byte so parseKeypress() can match\n // `[[A`, `[[B`, `[[5~`, etc.\n if (byte === 0x5b && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n continue\n }\n\n if (byte === 0x3f && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n this.state = { tag: \"csi_private_reply\", semicolons: 0, hasDigit: false, sawDollar: false }\n continue\n }\n\n if (byte === 0x3b) {\n const firstParamValue = parsePositiveDecimalPrefix(bytes, this.unitStart + 2, this.cursor)\n if (firstParamValue !== null) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: 1,\n segments: 1,\n hasDigit: false,\n firstParamValue,\n }\n continue\n }\n }\n\n // Standard CSI final byte (0x40–0x7E). Check for bracketed paste\n // start, SGR mouse, or a regular CSI key/response.\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n const rawBytes = bytes.subarray(this.unitStart, end)\n\n if (bytesEqual(rawBytes, BRACKETED_PASTE_START)) {\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n this.paste = createPasteCollector()\n continue\n }\n\n if (isMouseSgrSequence(rawBytes)) {\n this.emitMouse(rawBytes, \"sgr\")\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"csi_sgr_mouse\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.state = { tag: \"csi_sgr_mouse_deferred\", part: this.state.part, hasDigit: this.state.hasDigit }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: true }\n continue\n }\n\n if (byte === 0x3b && this.state.hasDigit && this.state.part < 2) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part + 1, hasDigit: false }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n const rawBytes = bytes.subarray(this.unitStart, end)\n if (isMouseSgrSequence(rawBytes)) {\n this.emitMouse(rawBytes, \"sgr\")\n } else {\n this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n }\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_sgr_mouse_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x4d || byte === 0x6d) {\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: this.state.hasDigit }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"csi_parametric\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n if (canDeferParametricCsi(this.state, this.protocolContext)) {\n this.state = {\n tag: \"csi_parametric_deferred\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: true,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte === 0x3a && this.state.hasDigit && this.state.segments < 3) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments + 1,\n hasDigit: false,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte === 0x3b && this.state.semicolons < 2) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons + 1,\n segments: 1,\n hasDigit: false,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_parametric_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3a || byte === 0x3b) {\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (canCompleteDeferredParametricCsi(this.state, byte, this.protocolContext)) {\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"csi_private_reply\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n if (canDeferPrivateReplyCsi(this.protocolContext)) {\n this.state = {\n tag: \"csi_private_reply_deferred\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: true,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n if (byte === 0x3b) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons + 1,\n hasDigit: false,\n sawDollar: false,\n }\n continue\n }\n\n if (byte === 0x24 && this.state.hasDigit && !this.state.sawDollar) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: true,\n sawDollar: true,\n }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_private_reply_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x24) {\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n if (canCompleteDeferredPrivateReplyCsi(this.state, byte, this.protocolContext)) {\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // OSC sequences end at BEL or ESC \\. DCS and APC end at ESC \\\n // only. The sawEsc flag tracks whether the previous byte was ESC,\n // since the two-byte ESC \\ can split across push() calls.\n case \"osc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"osc\", sawEsc: false }\n continue\n }\n\n if (byte === BEL) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"osc\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"dcs\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"dcs\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"dcs\", sawEsc: false }\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"dcs\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"apc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"apc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"apc\", sawEsc: false }\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"apc\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n // Delayed SGR mouse continuation after `esc_recovery` has consumed the\n // leading `[`. Consume the rest of `<digits;digits;digitsM/m` as one\n // opaque response so split mouse bytes never leak into text.\n case \"esc_less_mouse\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if ((byte >= 0x30 && byte <= 0x39) || byte === 0x3b) {\n this.cursor += 1\n continue\n }\n\n if (byte === 0x4d || byte === 0x6d) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // Delayed X10 mouse continuation after `esc_recovery` has consumed the\n // leading `[`. Consume `[M` plus its three raw payload bytes as one\n // opaque response so split mouse bytes never leak into text.\n case \"esc_less_x10_mouse\": {\n const end = this.unitStart + 5\n\n if (bytes.length < end) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n this.state = { tag: \"ground\" }\n this.consumePrefix(bytes.length)\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n }\n }\n }\n\n // Tries to parse the raw string as a key via parseKeypress(). If it\n // recognizes the sequence (printable char, arrow, function key, etc.),\n // emits a key event. Otherwise emits a response event — this is how\n // capability responses, focus sequences, and other non-key CSI traffic\n // avoids becoming text.\n private emitKeyOrResponse(protocol: StdinResponseProtocol, raw: string): void {\n const parsed = parseKeypress(raw, { useKittyKeyboard: this.useKittyKeyboard })\n if (parsed) {\n this.events.push({\n type: \"key\",\n raw: parsed.raw,\n key: parsed,\n })\n return\n }\n\n this.events.push({\n type: \"response\",\n protocol,\n sequence: raw,\n })\n }\n\n private emitMouse(rawBytes: Uint8Array, encoding: \"sgr\" | \"x10\"): void {\n const event = this.mouseParser.parseMouseEvent(rawBytes)\n if (!event) {\n this.emitOpaqueResponse(\"unknown\", rawBytes)\n return\n }\n\n this.events.push({\n type: \"mouse\",\n raw: decodeLatin1(rawBytes),\n encoding,\n event,\n })\n }\n\n // Handles single bytes in the 0x80–0xFF range that aren't valid UTF-8\n // leads. Passes them through parseKeypress() which maps them to the\n // existing meta-key behavior (e.g. Alt+letter in terminals that send\n // high bytes instead of ESC-prefixed sequences).\n private emitLegacyHighByte(byte: number): void {\n const parsed = parseKeypress(Buffer.from([byte]), { useKittyKeyboard: this.useKittyKeyboard })\n if (parsed) {\n this.events.push({\n type: \"key\",\n raw: parsed.raw,\n key: parsed,\n })\n return\n }\n\n this.events.push({\n type: \"response\",\n protocol: \"unknown\",\n sequence: String.fromCharCode(byte),\n })\n }\n\n private emitOpaqueResponse(protocol: StdinResponseProtocol, rawBytes: Uint8Array): void {\n this.events.push({\n type: \"response\",\n protocol,\n sequence: decodeLatin1(rawBytes),\n })\n }\n\n // Advances past a completed protocol unit. Resets cursor, unitStart,\n // and timeout state so the next scan iteration starts clean.\n private consumePrefix(endExclusive: number): void {\n this.pending.consume(endExclusive)\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n }\n\n // Removes all bytes from the pending queue and returns them. Used when\n // entering paste mode — leftover bytes after the paste start marker\n // need to flow through consumePasteBytes() instead.\n private takePendingBytes(): Uint8Array {\n const buffered = this.pending.take()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n return buffered\n }\n\n // Emits all pending bytes as one opaque response and clears the buffer.\n // This keeps the parser buffer bounded at maxPendingBytes without\n // dropping data or splitting it into per-character events.\n private flushPendingOverflow(): void {\n if (this.pending.length === 0) {\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", this.pending.view())\n this.pending.clear()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n this.state = { tag: \"ground\" }\n }\n\n // Records when incomplete data first appeared so flushTimeout() can\n // decide whether enough time has elapsed to force-flush it.\n private markPending(): void {\n this.pendingSinceMs = this.clock.now()\n }\n\n // Processes bytes during an active bracketed paste. Searches for the end\n // marker (ESC[201~) using a sliding tail window so the marker can split\n // across chunk boundaries. Bytes that can't be part of the end marker are\n // appended to the paste collector without decoding.\n //\n // Returns any bytes that follow the end marker — those go back through\n // normal parsing in the push() loop.\n private consumePasteBytes(chunk: Uint8Array): Uint8Array {\n const paste = this.paste!\n const combined = concatBytes(paste.tail, chunk)\n const endIndex = indexOfBytes(combined, BRACKETED_PASTE_END)\n\n if (endIndex !== -1) {\n this.pushPasteBytes(combined.subarray(0, endIndex))\n\n this.events.push({\n type: \"paste\",\n bytes: joinPasteBytes(paste.parts, paste.totalLength),\n })\n\n this.paste = null\n return combined.subarray(endIndex + BRACKETED_PASTE_END.length)\n }\n\n // Keep enough trailing bytes to detect an end marker split across chunks.\n // Everything before that point is safe to retain immediately.\n const keep = Math.min(BRACKETED_PASTE_END.length - 1, combined.length)\n const stableLength = combined.length - keep\n if (stableLength > 0) {\n this.pushPasteBytes(combined.subarray(0, stableLength))\n }\n\n paste.tail = Uint8Array.from(combined.subarray(stableLength))\n return EMPTY_BYTES\n }\n\n private pushPasteBytes(bytes: Uint8Array): void {\n if (bytes.length === 0) {\n return\n }\n\n // Copy here because subarray() inputs may alias the caller's chunk or the\n // parser's pending buffer across pushes. The emitted paste event must keep\n // the original bytes even if those backing buffers are later reused.\n this.paste!.parts.push(Uint8Array.from(bytes))\n this.paste!.totalLength += bytes.length\n }\n\n private reconcileDeferredStateWithProtocolContext(): void {\n switch (this.state.tag) {\n case \"csi_parametric_deferred\":\n if (!canDeferParametricCsi(this.state, this.protocolContext)) {\n this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n }\n return\n\n case \"csi_private_reply_deferred\":\n if (!canDeferPrivateReplyCsi(this.protocolContext)) {\n this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n }\n return\n }\n }\n\n // Arms or disarms the timeout after every push(). If there's an incomplete\n // unit in the buffer, starts a timer. When the timer fires, it sets\n // forceFlush so the next read() converts the incomplete unit into one\n // atomic event (e.g. a lone ESC becoming an Escape key).\n private reconcileTimeoutState(): void {\n if (!this.armTimeouts) {\n return\n }\n\n if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n this.clearTimeout()\n return\n }\n\n this.clearTimeout()\n this.timeoutId = this.clock.setTimeout(() => {\n this.timeoutId = null\n if (this.destroyed) {\n return\n }\n\n try {\n this.flushTimeout(this.clock.now())\n this.onTimeoutFlush?.()\n } catch (error) {\n console.error(\"stdin parser timeout flush failed\", error)\n }\n }, this.timeoutMs)\n }\n\n private clearTimeout(): void {\n if (!this.timeoutId) {\n return\n }\n\n this.clock.clearTimeout(this.timeoutId)\n this.timeoutId = null\n }\n\n // Clears all parser state: pending bytes, queued events, timeout tracking,\n // and any active paste collector. Called by both reset() (suspend/resume)\n // and destroy() to ensure no stale state survives.\n private resetState(): void {\n this.pending.reset(INITIAL_PENDING_CAPACITY)\n this.events.length = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n this.justFlushedEsc = false\n this.state = { tag: \"ground\" }\n this.cursor = 0\n this.unitStart = 0\n this.paste = null\n this.mouseParser.reset()\n }\n}\n",
25
+ "import { singleton } from \"./singleton.js\"\n\n/**\n * Environment variable registry\n *\n * Usage:\n * ```ts\n * import { registerEnvVar, env } from \"./lib/env.ts\";\n *\n * // Register environment variables\n * registerEnvVar({\n * name: \"DEBUG\",\n * description: \"Enable debug logging\",\n * type: \"boolean\",\n * default: false\n * });\n *\n * registerEnvVar({\n * name: \"PORT\",\n * description: \"Server port number\",\n * type: \"number\",\n * default: 3000\n * });\n *\n * // Access environment variables\n * if (env.DEBUG) {\n * console.log(\"Debug mode enabled\");\n * }\n *\n * const port = env.PORT; // number\n * ```\n */\n\nexport interface EnvVarConfig {\n name: string\n description: string\n default?: string | boolean | number\n type?: \"string\" | \"boolean\" | \"number\"\n}\n\nexport const envRegistry: Record<string, EnvVarConfig> = singleton(\"env-registry\", () => ({}))\n\nexport function registerEnvVar(config: EnvVarConfig): void {\n const existing = envRegistry[config.name]\n if (existing) {\n if (\n existing.description !== config.description ||\n existing.type !== config.type ||\n existing.default !== config.default\n ) {\n throw new Error(\n `Environment variable \"${config.name}\" is already registered with different configuration. ` +\n `Existing: ${JSON.stringify(existing)}, New: ${JSON.stringify(config)}`,\n )\n }\n return\n }\n envRegistry[config.name] = config\n}\n\nfunction normalizeBoolean(value: string): boolean {\n const lowerValue = value.toLowerCase()\n return [\"true\", \"1\", \"on\", \"yes\"].includes(lowerValue)\n}\n\nfunction parseEnvValue(config: EnvVarConfig): string | boolean | number {\n const envValue = process.env[config.name]\n\n if (envValue === undefined && config.default !== undefined) {\n return config.default\n }\n\n if (envValue === undefined) {\n throw new Error(`Required environment variable ${config.name} is not set. ${config.description}`)\n }\n\n switch (config.type) {\n case \"boolean\":\n return typeof envValue === \"boolean\" ? envValue : normalizeBoolean(envValue)\n case \"number\":\n const numValue = Number(envValue)\n if (isNaN(numValue)) {\n throw new Error(`Environment variable ${config.name} must be a valid number, got: ${envValue}`)\n }\n return numValue\n case \"string\":\n default:\n return envValue\n }\n}\n\nclass EnvStore {\n private parsedValues: Map<string, string | boolean | number> = new Map()\n\n get(key: string): any {\n if (this.parsedValues.has(key)) {\n return this.parsedValues.get(key)!\n }\n\n if (!(key in envRegistry)) {\n throw new Error(`Environment variable ${key} is not registered.`)\n }\n\n try {\n const value = parseEnvValue(envRegistry[key])\n this.parsedValues.set(key, value)\n return value\n } catch (error) {\n throw new Error(`Failed to parse env var ${key}: ${error instanceof Error ? error.message : String(error)}`)\n }\n }\n\n has(key: string): boolean {\n return key in envRegistry\n }\n\n clearCache(): void {\n this.parsedValues.clear()\n }\n}\n\nconst envStore = singleton(\"env-store\", () => new EnvStore())\n\nexport function clearEnvCache(): void {\n envStore.clearCache()\n}\n\nexport function generateEnvMarkdown(): string {\n const configs = Object.values(envRegistry)\n\n if (configs.length === 0) {\n return \"# Environment Variables\\n\\nNo environment variables registered.\\n\"\n }\n\n let markdown = \"# Environment Variables\\n\\n\"\n\n for (const config of configs) {\n markdown += `## ${config.name}\\n\\n`\n markdown += `${config.description}\\n\\n`\n\n markdown += `**Type:** \\`${config.type || \"string\"}\\` \\n`\n\n if (config.default !== undefined) {\n const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n markdown += `**Default:** \\`${defaultValue}\\`\\n`\n } else {\n markdown += \"**Default:** *Required*\\n\"\n }\n\n markdown += \"\\n\"\n }\n\n return markdown\n}\n\nexport function generateEnvColored(): string {\n const configs = Object.values(envRegistry)\n\n if (configs.length === 0) {\n return \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\nNo environment variables registered.\\n\"\n }\n\n let output = \"\\x1b[1;36mEnvironment Variables\\x1b[0m\\n\\n\"\n\n for (const config of configs) {\n output += `\\x1b[1;33m${config.name}\\x1b[0m\\n`\n output += `${config.description}\\n`\n output += `\\x1b[32mType:\\x1b[0m \\x1b[36m${config.type || \"string\"}\\x1b[0m\\n`\n\n if (config.default !== undefined) {\n const defaultValue = typeof config.default === \"string\" ? `\"${config.default}\"` : String(config.default)\n output += `\\x1b[32mDefault:\\x1b[0m \\x1b[35m${defaultValue}\\x1b[0m\\n`\n } else {\n output += `\\x1b[32mDefault:\\x1b[0m \\x1b[31mRequired\\x1b[0m\\n`\n }\n\n output += \"\\n\"\n }\n\n return output\n}\n\nexport const env = new Proxy({} as Record<string, any>, {\n get(target, prop: string) {\n if (typeof prop !== \"string\") {\n return undefined\n }\n return envStore.get(prop)\n },\n\n has(target, prop: string) {\n return envStore.has(prop)\n },\n\n ownKeys() {\n return Object.keys(envRegistry)\n },\n\n getOwnPropertyDescriptor(target, prop: string) {\n if (envStore.has(prop)) {\n return {\n enumerable: true,\n configurable: true,\n get: () => envStore.get(prop),\n }\n }\n return undefined\n },\n})\n",
26
+ "// Byte-level stdin parser that turns raw terminal input into typed StdinEvents.\n//\n// This replaces a two-phase token -> decode pipeline with a single state machine\n// that produces fully typed events (key, mouse, paste, response) directly from\n// bytes. The parser owns all byte framing and protocol recognition. It does NOT\n// own event dispatch — that belongs to KeyHandler and the renderer.\n\nimport { Buffer } from \"node:buffer\"\nimport { SystemClock, type Clock, type TimerHandle } from \"./clock.js\"\nimport { parseKeypress, type ParsedKey } from \"./parse.keypress.js\"\nimport { MouseParser, type RawMouseEvent } from \"./parse.mouse.js\"\nimport type { PasteMetadata } from \"./paste.js\"\n\nexport { SystemClock, type Clock, type TimerHandle } from \"./clock.js\"\n\nexport type StdinResponseProtocol = \"csi\" | \"osc\" | \"dcs\" | \"apc\" | \"unknown\"\n\n// The four event types the parser produces. Everything stdin sends becomes\n// exactly one of these.\nexport type StdinEvent =\n | {\n type: \"key\"\n raw: string\n key: ParsedKey\n }\n | {\n type: \"mouse\"\n raw: string\n encoding: \"sgr\" | \"x10\"\n event: RawMouseEvent\n }\n | {\n type: \"paste\"\n bytes: Uint8Array\n metadata?: PasteMetadata\n }\n | {\n type: \"response\"\n protocol: StdinResponseProtocol\n sequence: string\n }\n\nexport interface StdinParserProtocolContext {\n kittyKeyboardEnabled: boolean\n privateCapabilityRepliesActive: boolean\n pixelResolutionQueryActive: boolean\n explicitWidthCprActive: boolean\n}\n\nexport interface StdinParserOptions {\n timeoutMs?: number\n maxPendingBytes?: number\n armTimeouts?: boolean\n onTimeoutFlush?: () => void\n useKittyKeyboard?: boolean\n protocolContext?: Partial<StdinParserProtocolContext>\n clock?: Clock\n}\n\n// State machine tags for the byte scanner. Each tag represents which protocol\n// framing mode the parser is currently inside. The sawEsc flag in osc/dcs/apc\n// tracks whether the previous byte was ESC, since the two-byte ST terminator\n// (ESC \\) can split across push() calls.\ntype ParserState =\n | { tag: \"ground\" }\n | { tag: \"utf8\"; expected: number; seen: number }\n | { tag: \"esc\" }\n | { tag: \"ss3\" }\n | { tag: \"csi\" }\n | { tag: \"csi_sgr_mouse\"; part: number; hasDigit: boolean }\n | { tag: \"csi_sgr_mouse_deferred\"; part: number; hasDigit: boolean }\n | { tag: \"csi_parametric\"; semicolons: number; segments: number; hasDigit: boolean; firstParamValue: number | null }\n | {\n tag: \"csi_parametric_deferred\"\n semicolons: number\n segments: number\n hasDigit: boolean\n firstParamValue: number | null\n }\n | { tag: \"csi_private_reply\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n | { tag: \"csi_private_reply_deferred\"; semicolons: number; hasDigit: boolean; sawDollar: boolean }\n | { tag: \"osc\"; sawEsc: boolean }\n | { tag: \"dcs\"; sawEsc: boolean }\n | { tag: \"apc\"; sawEsc: boolean }\n | { tag: \"esc_recovery\" }\n | { tag: \"esc_less_mouse\" }\n | { tag: \"esc_less_x10_mouse\" }\n\n// Collects paste body incrementally, bypassing the main ByteQueue so large\n// pastes don't grow the parser buffer. Keeps only a small tail for end-marker\n// detection across chunk boundaries.\ninterface PasteCollector {\n tail: Uint8Array\n parts: Uint8Array[]\n totalLength: number\n}\n\n// 10ms is enough to distinguish a lone ESC keypress from the start of an\n// escape sequence on all but the slowest connections.\nconst DEFAULT_TIMEOUT_MS = 10\nconst DEFAULT_MAX_PENDING_BYTES = 64 * 1024\nconst INITIAL_PENDING_CAPACITY = 256\nconst ESC = 0x1b\nconst BEL = 0x07\nconst BRACKETED_PASTE_START = Buffer.from(\"\\x1b[200~\")\nconst BRACKETED_PASTE_END = Buffer.from(\"\\x1b[201~\")\nconst EMPTY_BYTES = new Uint8Array(0)\nconst KEY_DECODER = new TextDecoder()\nconst DEFAULT_PROTOCOL_CONTEXT: StdinParserProtocolContext = {\n kittyKeyboardEnabled: false,\n privateCapabilityRepliesActive: false,\n pixelResolutionQueryActive: false,\n explicitWidthCprActive: false,\n}\n// rxvt uses $-terminated CSI sequences for shifted function keys (e.g. ESC[2$).\n// Standard CSI treats $ as an intermediate byte, not a final, so we match these\n// explicitly to avoid waiting for a \"real\" final byte that never arrives.\nconst RXVT_DOLLAR_CSI_RE = /^\\x1b\\[\\d+\\$$/\n\nconst SYSTEM_CLOCK = new SystemClock()\n\n// Byte buffer for pending input. Uses start/end offsets so consume() just\n// advances the start pointer without copying. Compacts (via copyWithin) only\n// when the consumed prefix exceeds half the buffer, keeping amortized cost low.\nclass ByteQueue {\n private buf: Uint8Array\n private start = 0\n private end = 0\n\n constructor(capacity = INITIAL_PENDING_CAPACITY) {\n this.buf = new Uint8Array(capacity)\n }\n\n get length(): number {\n return this.end - this.start\n }\n\n get capacity(): number {\n return this.buf.length\n }\n\n view(): Uint8Array {\n return this.buf.subarray(this.start, this.end)\n }\n\n // Returns a view of the contents and resets the queue. The view shares\n // the underlying buffer, so it becomes invalid on the next append().\n take(): Uint8Array {\n const chunk = this.view()\n this.start = 0\n this.end = 0\n return chunk\n }\n\n append(chunk: Uint8Array): void {\n if (chunk.length === 0) {\n return\n }\n\n this.ensureCapacity(this.length + chunk.length)\n this.buf.set(chunk, this.end)\n this.end += chunk.length\n }\n\n // Drops the first `count` bytes. Compacts when the consumed prefix\n // exceeds half the buffer to reclaim wasted space at the front.\n consume(count: number): void {\n if (count <= 0) {\n return\n }\n\n if (count >= this.length) {\n this.start = 0\n this.end = 0\n return\n }\n\n this.start += count\n if (this.start >= this.buf.length / 2) {\n this.buf.copyWithin(0, this.start, this.end)\n this.end -= this.start\n this.start = 0\n }\n }\n\n clear(): void {\n this.start = 0\n this.end = 0\n }\n\n reset(capacity = INITIAL_PENDING_CAPACITY): void {\n this.buf = new Uint8Array(capacity)\n this.start = 0\n this.end = 0\n }\n\n // Tries reclaiming space by compacting data to the front first.\n // Doubles the allocation if that still isn't enough.\n private ensureCapacity(requiredLength: number): void {\n const currentLength = this.length\n if (requiredLength <= this.buf.length) {\n const availableAtEnd = this.buf.length - this.end\n if (availableAtEnd >= requiredLength - currentLength) {\n return\n }\n\n this.buf.copyWithin(0, this.start, this.end)\n this.end = currentLength\n this.start = 0\n if (requiredLength <= this.buf.length) {\n return\n }\n }\n\n let nextCapacity = this.buf.length\n while (nextCapacity < requiredLength) {\n nextCapacity *= 2\n }\n\n const next = new Uint8Array(nextCapacity)\n next.set(this.view(), 0)\n this.buf = next\n this.start = 0\n this.end = currentLength\n }\n}\n\nfunction normalizePositiveOption(value: number | undefined, fallback: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0) {\n return fallback\n }\n\n return Math.floor(value)\n}\n\n// Returns the expected byte count for a UTF-8 sequence given its lead byte,\n// or 0 for bytes that aren't valid UTF-8 leads. Returning 0 tells the parser\n// this is a legacy high-byte character (0x80–0xBF, 0xC0–0xC1, 0xF5+) that\n// goes through the parseKeypress() meta-key path instead.\nfunction utf8SequenceLength(first: number): number {\n if (first < 0x80) return 1\n if (first >= 0xc2 && first <= 0xdf) return 2\n if (first >= 0xe0 && first <= 0xef) return 3\n if (first >= 0xf0 && first <= 0xf4) return 4\n return 0\n}\n\nfunction bytesEqual(left: Uint8Array, right: Uint8Array): boolean {\n if (left.length !== right.length) {\n return false\n }\n\n for (let index = 0; index < left.length; index += 1) {\n if (left[index] !== right[index]) {\n return false\n }\n }\n\n return true\n}\n\n// Checks whether a byte sequence is a complete SGR mouse report:\n// ESC [ < Ps ; Ps ; Ps M/m (three semicolon-separated digit groups).\nfunction isMouseSgrSequence(sequence: Uint8Array): boolean {\n if (sequence.length < 7) {\n return false\n }\n\n if (sequence[0] !== ESC || sequence[1] !== 0x5b || sequence[2] !== 0x3c) {\n return false\n }\n\n const final = sequence[sequence.length - 1]\n if (final !== 0x4d && final !== 0x6d) {\n return false\n }\n\n let part = 0\n let hasDigit = false\n for (let index = 3; index < sequence.length - 1; index += 1) {\n const byte = sequence[index]!\n if (byte >= 0x30 && byte <= 0x39) {\n hasDigit = true\n continue\n }\n\n if (byte === 0x3b && hasDigit && part < 2) {\n part += 1\n hasDigit = false\n continue\n }\n\n return false\n }\n\n return part === 2 && hasDigit\n}\n\nfunction isAsciiDigit(byte: number): boolean {\n return byte >= 0x30 && byte <= 0x39\n}\n\ninterface ParametricCsiLike {\n semicolons: number\n segments: number\n hasDigit: boolean\n firstParamValue: number | null\n}\n\ninterface PrivateReplyCsiLike {\n semicolons: number\n hasDigit: boolean\n sawDollar: boolean\n}\n\nfunction parsePositiveDecimalPrefix(sequence: Uint8Array, start: number, endExclusive: number): number | null {\n if (start >= endExclusive) return null\n\n let value = 0\n let sawDigit = false\n for (let index = start; index < endExclusive; index += 1) {\n const byte = sequence[index]!\n if (!isAsciiDigit(byte)) return null\n sawDigit = true\n value = value * 10 + (byte - 0x30)\n }\n\n return sawDigit ? value : null\n}\n\nfunction canStillBeKittyU(state: ParametricCsiLike): boolean {\n return state.semicolons >= 1\n}\n\nfunction canStillBeKittySpecial(state: ParametricCsiLike): boolean {\n return state.semicolons === 1 && state.segments > 1\n}\n\nfunction canStillBeExplicitWidthCpr(state: ParametricCsiLike): boolean {\n return state.firstParamValue === 1 && state.semicolons === 1\n}\n\nfunction canStillBePixelResolution(state: ParametricCsiLike): boolean {\n return state.firstParamValue === 4 && state.semicolons === 2\n}\n\nfunction canDeferParametricCsi(state: ParametricCsiLike, context: StdinParserProtocolContext): boolean {\n return (\n (context.kittyKeyboardEnabled && (canStillBeKittyU(state) || canStillBeKittySpecial(state))) ||\n (context.explicitWidthCprActive && canStillBeExplicitWidthCpr(state)) ||\n (context.pixelResolutionQueryActive && canStillBePixelResolution(state))\n )\n}\n\nfunction canCompleteDeferredParametricCsi(\n state: ParametricCsiLike,\n byte: number,\n context: StdinParserProtocolContext,\n): boolean {\n if (context.kittyKeyboardEnabled) {\n if (state.hasDigit && byte === 0x75) return true\n if (\n state.hasDigit &&\n state.semicolons === 1 &&\n state.segments > 1 &&\n (byte === 0x7e || (byte >= 0x41 && byte <= 0x5a))\n ) {\n return true\n }\n }\n\n if (\n context.explicitWidthCprActive &&\n state.hasDigit &&\n state.firstParamValue === 1 &&\n state.semicolons === 1 &&\n byte === 0x52\n ) {\n return true\n }\n\n if (\n context.pixelResolutionQueryActive &&\n state.hasDigit &&\n state.firstParamValue === 4 &&\n state.semicolons === 2 &&\n byte === 0x74\n ) {\n return true\n }\n\n return false\n}\n\nfunction canDeferPrivateReplyCsi(context: StdinParserProtocolContext): boolean {\n return context.privateCapabilityRepliesActive\n}\n\nfunction canCompleteDeferredPrivateReplyCsi(\n state: PrivateReplyCsiLike,\n byte: number,\n context: StdinParserProtocolContext,\n): boolean {\n if (!context.privateCapabilityRepliesActive) return false\n if (state.sawDollar) return state.hasDigit && byte === 0x79\n if (byte === 0x63) return state.hasDigit || state.semicolons > 0\n return state.hasDigit && byte === 0x75\n}\n\nfunction concatBytes(left: Uint8Array, right: Uint8Array): Uint8Array {\n if (left.length === 0) {\n return right\n }\n\n if (right.length === 0) {\n return left\n }\n\n const combined = new Uint8Array(left.length + right.length)\n combined.set(left, 0)\n combined.set(right, left.length)\n return combined\n}\n\nfunction indexOfBytes(haystack: Uint8Array, needle: Uint8Array): number {\n if (needle.length === 0) {\n return 0\n }\n\n const limit = haystack.length - needle.length\n for (let offset = 0; offset <= limit; offset += 1) {\n let matched = true\n for (let index = 0; index < needle.length; index += 1) {\n if (haystack[offset + index] !== needle[index]) {\n matched = false\n break\n }\n }\n\n if (matched) {\n return offset\n }\n }\n\n return -1\n}\n\n// Decodes raw protocol bytes as latin1. Used for mouse and response events\n// where the wire bytes may not be valid UTF-8 but need a lossless string\n// form for downstream sequence handlers.\nfunction decodeLatin1(bytes: Uint8Array): string {\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString(\"latin1\")\n}\n\nfunction decodeUtf8(bytes: Uint8Array): string {\n return KEY_DECODER.decode(bytes)\n}\n\nfunction createPasteCollector(): PasteCollector {\n return {\n tail: EMPTY_BYTES,\n parts: [],\n totalLength: 0,\n }\n}\n\nfunction joinPasteBytes(parts: Uint8Array[], totalLength: number): Uint8Array {\n if (totalLength === 0) {\n return EMPTY_BYTES\n }\n\n if (parts.length === 1) {\n return parts[0]!\n }\n\n const bytes = new Uint8Array(totalLength)\n let offset = 0\n for (const part of parts) {\n bytes.set(part, offset)\n offset += part.length\n }\n\n return bytes\n}\n\n// Push-driven stdin parser. Callers feed raw bytes via push(), then read\n// typed events via read() or drain(). At most one incomplete protocol unit\n// is buffered at a time; everything else is immediately converted to events.\n//\n// The parser guarantees chunk-shape invariance: the same bytes always produce\n// the same events, regardless of chunk boundaries. A lone ESC resolves via\n// timeout, split UTF-8 codepoints reassemble correctly, and bracketed paste\n// markers may split across any chunk boundary.\nexport class StdinParser {\n private readonly pending = new ByteQueue(INITIAL_PENDING_CAPACITY)\n private readonly events: StdinEvent[] = []\n private readonly timeoutMs: number\n private readonly maxPendingBytes: number\n private readonly armTimeouts: boolean\n private readonly onTimeoutFlush: (() => void) | null\n private readonly useKittyKeyboard: boolean\n private readonly mouseParser = new MouseParser()\n private readonly clock: Clock\n private protocolContext: StdinParserProtocolContext\n private timeoutId: TimerHandle | null = null\n private destroyed = false\n // When the current incomplete unit first appeared. Null when nothing is pending.\n private pendingSinceMs: number | null = null\n // When true, the state machine treats the current incomplete prefix as\n // final and emits it as one atomic event (e.g. a lone ESC becomes an\n // Escape key). Set by the timeout, consumed by the next read() or drain().\n private forceFlush = false\n // True only immediately after a timeout flush emits a lone ESC key. The next\n // `[` may begin a delayed `[<...M/m` mouse continuation recovery path.\n private justFlushedEsc = false\n private state: ParserState = { tag: \"ground\" }\n // Scan position within pending.view() during scanPending().\n private cursor = 0\n // Start of the protocol unit currently being parsed. The bytes from\n // unitStart through cursor all belong to one atomic unit.\n private unitStart = 0\n // When non-null, the parser is inside a bracketed paste. All incoming\n // bytes flow through consumePasteBytes() instead of the normal state machine.\n private paste: PasteCollector | null = null\n\n constructor(options: StdinParserOptions = {}) {\n this.timeoutMs = normalizePositiveOption(options.timeoutMs, DEFAULT_TIMEOUT_MS)\n this.maxPendingBytes = normalizePositiveOption(options.maxPendingBytes, DEFAULT_MAX_PENDING_BYTES)\n this.armTimeouts = options.armTimeouts ?? true\n this.onTimeoutFlush = options.onTimeoutFlush ?? null\n this.useKittyKeyboard = options.useKittyKeyboard ?? true\n this.clock = options.clock ?? SYSTEM_CLOCK\n this.protocolContext = {\n ...DEFAULT_PROTOCOL_CONTEXT,\n kittyKeyboardEnabled: options.protocolContext?.kittyKeyboardEnabled ?? false,\n privateCapabilityRepliesActive: options.protocolContext?.privateCapabilityRepliesActive ?? false,\n pixelResolutionQueryActive: options.protocolContext?.pixelResolutionQueryActive ?? false,\n explicitWidthCprActive: options.protocolContext?.explicitWidthCprActive ?? false,\n }\n }\n\n public get bufferCapacity(): number {\n return this.pending.capacity\n }\n\n public updateProtocolContext(patch: Partial<StdinParserProtocolContext>): void {\n this.ensureAlive()\n this.protocolContext = { ...this.protocolContext, ...patch }\n this.reconcileDeferredStateWithProtocolContext()\n this.reconcileTimeoutState()\n }\n\n // Feeds raw stdin bytes into the parser. Converts as much as possible into\n // queued events and leaves at most one incomplete unit behind in pending.\n //\n // When a chunk contains a paste start marker, bytes before the marker go\n // through normal parsing, then paste mode takes over for the rest. This\n // prevents large pastes from growing the main buffer.\n public push(data: Uint8Array): void {\n this.ensureAlive()\n if (data.length === 0) {\n // Preserve the existing empty-chunk -> empty-keypress behavior.\n this.emitKeyOrResponse(\"unknown\", \"\")\n return\n }\n\n let remainder = data\n while (remainder.length > 0) {\n if (this.paste) {\n remainder = this.consumePasteBytes(remainder)\n continue\n }\n\n // If we're in ground state with nothing pending, scan the incoming\n // chunk for a paste start marker. Only append through the marker so\n // scanPending() enters paste mode without buffering the full paste.\n const immediatePasteStartIndex =\n this.state.tag === \"ground\" && this.pending.length === 0 ? indexOfBytes(remainder, BRACKETED_PASTE_START) : -1\n const appendEnd =\n immediatePasteStartIndex === -1 ? remainder.length : immediatePasteStartIndex + BRACKETED_PASTE_START.length\n\n this.pending.append(remainder.subarray(0, appendEnd))\n remainder = remainder.subarray(appendEnd)\n this.scanPending()\n\n if (this.paste && this.pending.length > 0) {\n remainder = this.consumePasteBytes(this.takePendingBytes())\n continue\n }\n\n if (!this.paste && this.pending.length > this.maxPendingBytes) {\n this.flushPendingOverflow()\n this.scanPending()\n\n if (this.paste && this.pending.length > 0) {\n remainder = this.consumePasteBytes(this.takePendingBytes())\n }\n }\n }\n\n this.reconcileTimeoutState()\n }\n\n // Pops one event from the queue. If the queue is empty and a timeout has\n // set forceFlush, re-scans pending to convert the timed-out incomplete\n // unit into one final event before returning it.\n public read(): StdinEvent | null {\n this.ensureAlive()\n\n if (this.events.length === 0 && this.forceFlush) {\n this.scanPending()\n this.reconcileTimeoutState()\n }\n\n return this.events.shift() ?? null\n }\n\n // Delivers all queued events. Stops early if the parser is destroyed\n // during a callback (e.g. an event handler triggers teardown).\n public drain(onEvent: (event: StdinEvent) => void): void {\n this.ensureAlive()\n\n while (true) {\n if (this.destroyed) {\n return\n }\n\n const event = this.read()\n if (!event) {\n return\n }\n\n onEvent(event)\n }\n }\n\n // Marks the parser for forced flush if enough time has passed since\n // incomplete data arrived. Does not immediately emit events — the next\n // read() or drain() does the actual flush. This separation keeps the\n // timer callback from emitting events mid-flight in user code.\n public flushTimeout(nowMsValue: number = this.clock.now()): void {\n this.ensureAlive()\n\n if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n return\n }\n\n if (nowMsValue < this.pendingSinceMs || nowMsValue - this.pendingSinceMs < this.timeoutMs) {\n return\n }\n\n this.forceFlush = true\n }\n\n public reset(): void {\n if (this.destroyed) {\n return\n }\n\n this.clearTimeout()\n this.resetState()\n }\n\n public resetMouseState(): void {\n this.ensureAlive()\n this.mouseParser.reset()\n }\n\n public destroy(): void {\n if (this.destroyed) {\n return\n }\n\n this.clearTimeout()\n this.destroyed = true\n this.resetState()\n }\n\n private ensureAlive(): void {\n if (this.destroyed) {\n throw new Error(\"StdinParser has been destroyed\")\n }\n }\n\n // Scans the pending byte buffer one byte at a time, dispatching on the\n // current parser state. All protocol framing lives in this single switch\n // — intentionally not split into per-mode scan helpers.\n //\n // Exits when: all bytes consumed (ground), more bytes needed (incomplete\n // unit), or paste mode entered (body handled by consumePasteBytes).\n private scanPending(): void {\n while (!this.paste) {\n const bytes = this.pending.view()\n if (this.state.tag === \"ground\" && this.cursor >= bytes.length) {\n this.pending.clear()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n const byte = this.cursor < bytes.length ? bytes[this.cursor]! : -1\n switch (this.state.tag) {\n case \"ground\": {\n this.unitStart = this.cursor\n\n // After a timeout-flushed lone ESC, a following `[` may be the start\n // of a delayed `[<...M/m` mouse continuation. Recover only this narrow\n // case; otherwise clear the recovery flag and parse bytes normally.\n if (this.justFlushedEsc) {\n if (byte === 0x5b) {\n this.justFlushedEsc = false\n this.cursor += 1\n this.state = { tag: \"esc_recovery\" }\n continue\n }\n\n this.justFlushedEsc = false\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"esc\" }\n continue\n }\n\n if (byte < 0x80) {\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.cursor, this.cursor + 1)))\n this.consumePrefix(this.cursor + 1)\n continue\n }\n\n // Invalid UTF-8 lead byte. Could be a legacy high-byte from an\n // older terminal. If it's the last byte in the buffer, wait for\n // more data or a timeout before committing. On timeout, emit\n // through parseKeypress() which handles meta-key behavior.\n const expected = utf8SequenceLength(byte)\n if (expected === 0) {\n if (!this.forceFlush && this.cursor + 1 === bytes.length) {\n this.markPending()\n return\n }\n\n this.emitLegacyHighByte(byte)\n this.consumePrefix(this.cursor + 1)\n continue\n }\n\n this.cursor += 1\n this.state = { tag: \"utf8\", expected, seen: 1 }\n continue\n }\n\n case \"utf8\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitLegacyHighByte(bytes[this.unitStart]!)\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n // Not a valid continuation byte. Treat the lead byte as a legacy\n // high-byte character and restart parsing from this position.\n if ((byte & 0xc0) !== 0x80) {\n this.emitLegacyHighByte(bytes[this.unitStart]!)\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n const nextSeen = this.state.seen + 1\n this.cursor += 1\n if (nextSeen < this.state.expected) {\n this.state = { tag: \"utf8\", expected: this.state.expected, seen: nextSeen }\n continue\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"esc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n const flushedLoneEsc = this.cursor === this.unitStart + 1 && bytes[this.unitStart] === ESC\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.justFlushedEsc = flushedLoneEsc\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // The byte after ESC determines the sub-protocol:\n // [ -> CSI, O -> SS3, ] -> OSC, P -> DCS, _ -> APC.\n switch (byte) {\n case 0x5b:\n this.cursor += 1\n this.state = { tag: \"csi\" }\n continue\n case 0x4f:\n this.cursor += 1\n this.state = { tag: \"ss3\" }\n continue\n case 0x5d:\n this.cursor += 1\n this.state = { tag: \"osc\", sawEsc: false }\n continue\n case 0x50:\n this.cursor += 1\n this.state = { tag: \"dcs\", sawEsc: false }\n continue\n case 0x5f:\n this.cursor += 1\n this.state = { tag: \"apc\", sawEsc: false }\n continue\n // ESC ESC: stay in esc state. Terminals encode Alt+ESC and\n // similar sequences as ESC ESC [...], so we keep scanning.\n case ESC:\n this.cursor += 1\n continue\n default:\n this.cursor += 1\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n }\n\n case \"ss3\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n this.cursor += 1\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // Narrow recovery path for delayed mouse continuations after a\n // timeout-flushed lone ESC. Wait for either `<` (SGR) or `M` (X10); if\n // neither arrives, flush `[` as a normal key.\n case \"esc_recovery\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.cursor)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === 0x3c) {\n this.cursor += 1\n this.state = { tag: \"esc_less_mouse\" }\n continue\n }\n\n if (byte === 0x4d) {\n this.cursor += 1\n this.state = { tag: \"esc_less_x10_mouse\" }\n continue\n }\n\n this.emitKeyOrResponse(\"unknown\", decodeUtf8(bytes.subarray(this.unitStart, this.unitStart + 1)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.unitStart + 1)\n continue\n }\n\n case \"csi\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // A new ESC inside an incomplete CSI means the previous sequence\n // was interrupted. Flush everything before the new ESC as one\n // opaque response, then restart parsing at the new ESC.\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // X10 mouse: ESC [ M plus 3 raw payload bytes (button, x, y).\n // cursor === unitStart + 2 confirms M comes right after ESC[,\n // not as a later final byte in a different CSI sequence.\n if (byte === 0x4d && this.cursor === this.unitStart + 2) {\n const end = this.cursor + 4\n if (bytes.length < end) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n this.state = { tag: \"ground\" }\n this.consumePrefix(bytes.length)\n continue\n }\n\n this.emitMouse(bytes.subarray(this.unitStart, end), \"x10\")\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n if (byte === 0x24) {\n const candidateEnd = this.cursor + 1\n const candidate = decodeUtf8(bytes.subarray(this.unitStart, candidateEnd))\n if (RXVT_DOLLAR_CSI_RE.test(candidate)) {\n this.emitKeyOrResponse(\"csi\", candidate)\n this.state = { tag: \"ground\" }\n this.consumePrefix(candidateEnd)\n continue\n }\n\n if (!this.forceFlush && candidateEnd >= bytes.length) {\n this.markPending()\n return\n }\n }\n\n if (byte === 0x3c && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: 0, hasDigit: false }\n continue\n }\n\n // Some terminals use ESC [[A..E / ESC [[5~ / ESC [[6~ variants.\n // Treat the second `[` immediately after ESC[ as part of the CSI\n // payload instead of as a final byte so parseKeypress() can match\n // `[[A`, `[[B`, `[[5~`, etc.\n if (byte === 0x5b && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n continue\n }\n\n if (byte === 0x3f && this.cursor === this.unitStart + 2) {\n this.cursor += 1\n this.state = { tag: \"csi_private_reply\", semicolons: 0, hasDigit: false, sawDollar: false }\n continue\n }\n\n if (byte === 0x3b) {\n const firstParamValue = parsePositiveDecimalPrefix(bytes, this.unitStart + 2, this.cursor)\n if (firstParamValue !== null) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: 1,\n segments: 1,\n hasDigit: false,\n firstParamValue,\n }\n continue\n }\n }\n\n // Standard CSI final byte (0x40–0x7E). Check for bracketed paste\n // start, SGR mouse, or a regular CSI key/response.\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n const rawBytes = bytes.subarray(this.unitStart, end)\n\n if (bytesEqual(rawBytes, BRACKETED_PASTE_START)) {\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n this.paste = createPasteCollector()\n continue\n }\n\n if (isMouseSgrSequence(rawBytes)) {\n this.emitMouse(rawBytes, \"sgr\")\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"csi_sgr_mouse\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.state = { tag: \"csi_sgr_mouse_deferred\", part: this.state.part, hasDigit: this.state.hasDigit }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: true }\n continue\n }\n\n if (byte === 0x3b && this.state.hasDigit && this.state.part < 2) {\n this.cursor += 1\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part + 1, hasDigit: false }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n const rawBytes = bytes.subarray(this.unitStart, end)\n if (isMouseSgrSequence(rawBytes)) {\n this.emitMouse(rawBytes, \"sgr\")\n } else {\n this.emitKeyOrResponse(\"csi\", decodeUtf8(rawBytes))\n }\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_sgr_mouse_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x4d || byte === 0x6d) {\n this.state = { tag: \"csi_sgr_mouse\", part: this.state.part, hasDigit: this.state.hasDigit }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"csi_parametric\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n if (canDeferParametricCsi(this.state, this.protocolContext)) {\n this.state = {\n tag: \"csi_parametric_deferred\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: true,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte === 0x3a && this.state.hasDigit && this.state.segments < 3) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments + 1,\n hasDigit: false,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte === 0x3b && this.state.semicolons < 2) {\n this.cursor += 1\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons + 1,\n segments: 1,\n hasDigit: false,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_parametric_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3a || byte === 0x3b) {\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n if (canCompleteDeferredParametricCsi(this.state, byte, this.protocolContext)) {\n this.state = {\n tag: \"csi_parametric\",\n semicolons: this.state.semicolons,\n segments: this.state.segments,\n hasDigit: this.state.hasDigit,\n firstParamValue: this.state.firstParamValue,\n }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n case \"csi_private_reply\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n if (canDeferPrivateReplyCsi(this.protocolContext)) {\n this.state = {\n tag: \"csi_private_reply_deferred\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte)) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: true,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n if (byte === 0x3b) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons + 1,\n hasDigit: false,\n sawDollar: false,\n }\n continue\n }\n\n if (byte === 0x24 && this.state.hasDigit && !this.state.sawDollar) {\n this.cursor += 1\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: true,\n sawDollar: true,\n }\n continue\n }\n\n if (byte >= 0x40 && byte <= 0x7e) {\n const end = this.cursor + 1\n this.emitKeyOrResponse(\"csi\", decodeUtf8(bytes.subarray(this.unitStart, end)))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"csi\" }\n continue\n }\n\n case \"csi_private_reply_deferred\": {\n if (this.cursor >= bytes.length) {\n this.pendingSinceMs = null\n this.forceFlush = false\n return\n }\n\n if (byte === ESC) {\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (isAsciiDigit(byte) || byte === 0x3b || byte === 0x24) {\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n if (canCompleteDeferredPrivateReplyCsi(this.state, byte, this.protocolContext)) {\n this.state = {\n tag: \"csi_private_reply\",\n semicolons: this.state.semicolons,\n hasDigit: this.state.hasDigit,\n sawDollar: this.state.sawDollar,\n }\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // OSC sequences end at BEL or ESC \\. DCS and APC end at ESC \\\n // only. The sawEsc flag tracks whether the previous byte was ESC,\n // since the two-byte ESC \\ can split across push() calls.\n case \"osc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"osc\", sawEsc: false }\n continue\n }\n\n if (byte === BEL) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"osc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"osc\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"dcs\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"dcs\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"dcs\", sawEsc: false }\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"dcs\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n case \"apc\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if (this.state.sawEsc) {\n if (byte === 0x5c) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"apc\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.state = { tag: \"apc\", sawEsc: false }\n continue\n }\n\n if (byte === ESC) {\n this.cursor += 1\n this.state = { tag: \"apc\", sawEsc: true }\n continue\n }\n\n this.cursor += 1\n continue\n }\n\n // Delayed SGR mouse continuation after `esc_recovery` has consumed the\n // leading `[`. Consume the rest of `<digits;digits;digitsM/m` as one\n // opaque response so split mouse bytes never leak into text.\n case \"esc_less_mouse\": {\n if (this.cursor >= bytes.length) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n if ((byte >= 0x30 && byte <= 0x39) || byte === 0x3b) {\n this.cursor += 1\n continue\n }\n\n if (byte === 0x4d || byte === 0x6d) {\n const end = this.cursor + 1\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n continue\n }\n\n // Delayed X10 mouse continuation after `esc_recovery` has consumed the\n // leading `[`. Consume `[M` plus its three raw payload bytes as one\n // opaque response so split mouse bytes never leak into text.\n case \"esc_less_x10_mouse\": {\n const end = this.unitStart + 5\n\n if (bytes.length < end) {\n if (!this.forceFlush) {\n this.markPending()\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, bytes.length))\n this.state = { tag: \"ground\" }\n this.consumePrefix(bytes.length)\n continue\n }\n\n this.emitOpaqueResponse(\"unknown\", bytes.subarray(this.unitStart, end))\n this.state = { tag: \"ground\" }\n this.consumePrefix(end)\n continue\n }\n }\n }\n }\n\n // Tries to parse the raw string as a key via parseKeypress(). If it\n // recognizes the sequence (printable char, arrow, function key, etc.),\n // emits a key event. Otherwise emits a response event — this is how\n // capability responses, focus sequences, and other non-key CSI traffic\n // avoids becoming text.\n private emitKeyOrResponse(protocol: StdinResponseProtocol, raw: string): void {\n const parsed = parseKeypress(raw, { useKittyKeyboard: this.useKittyKeyboard })\n if (parsed) {\n this.events.push({\n type: \"key\",\n raw: parsed.raw,\n key: parsed,\n })\n return\n }\n\n this.events.push({\n type: \"response\",\n protocol,\n sequence: raw,\n })\n }\n\n private emitMouse(rawBytes: Uint8Array, encoding: \"sgr\" | \"x10\"): void {\n const event = this.mouseParser.parseMouseEvent(rawBytes)\n if (!event) {\n this.emitOpaqueResponse(\"unknown\", rawBytes)\n return\n }\n\n this.events.push({\n type: \"mouse\",\n raw: decodeLatin1(rawBytes),\n encoding,\n event,\n })\n }\n\n // Handles single bytes in the 0x80–0xFF range that aren't valid UTF-8\n // leads. Passes them through parseKeypress() which maps them to the\n // existing meta-key behavior (e.g. Alt+letter in terminals that send\n // high bytes instead of ESC-prefixed sequences).\n private emitLegacyHighByte(byte: number): void {\n const parsed = parseKeypress(Buffer.from([byte]), { useKittyKeyboard: this.useKittyKeyboard })\n if (parsed) {\n this.events.push({\n type: \"key\",\n raw: parsed.raw,\n key: parsed,\n })\n return\n }\n\n this.events.push({\n type: \"response\",\n protocol: \"unknown\",\n sequence: String.fromCharCode(byte),\n })\n }\n\n private emitOpaqueResponse(protocol: StdinResponseProtocol, rawBytes: Uint8Array): void {\n this.events.push({\n type: \"response\",\n protocol,\n sequence: decodeLatin1(rawBytes),\n })\n }\n\n // Advances past a completed protocol unit. Resets cursor, unitStart,\n // and timeout state so the next scan iteration starts clean.\n private consumePrefix(endExclusive: number): void {\n this.pending.consume(endExclusive)\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n }\n\n // Removes all bytes from the pending queue and returns them. Used when\n // entering paste mode — leftover bytes after the paste start marker\n // need to flow through consumePasteBytes() instead.\n private takePendingBytes(): Uint8Array {\n const buffered = this.pending.take()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n return buffered\n }\n\n // Emits all pending bytes as one opaque response and clears the buffer.\n // This keeps the parser buffer bounded at maxPendingBytes without\n // dropping data or splitting it into per-character events.\n private flushPendingOverflow(): void {\n if (this.pending.length === 0) {\n return\n }\n\n this.emitOpaqueResponse(\"unknown\", this.pending.view())\n this.pending.clear()\n this.cursor = 0\n this.unitStart = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n this.state = { tag: \"ground\" }\n }\n\n // Records when incomplete data first appeared so flushTimeout() can\n // decide whether enough time has elapsed to force-flush it.\n private markPending(): void {\n this.pendingSinceMs = this.clock.now()\n }\n\n // Processes bytes during an active bracketed paste. Searches for the end\n // marker (ESC[201~) using a sliding tail window so the marker can split\n // across chunk boundaries. Bytes that can't be part of the end marker are\n // appended to the paste collector without decoding.\n //\n // Returns any bytes that follow the end marker — those go back through\n // normal parsing in the push() loop.\n private consumePasteBytes(chunk: Uint8Array): Uint8Array {\n const paste = this.paste!\n const combined = concatBytes(paste.tail, chunk)\n const endIndex = indexOfBytes(combined, BRACKETED_PASTE_END)\n\n if (endIndex !== -1) {\n this.pushPasteBytes(combined.subarray(0, endIndex))\n\n this.events.push({\n type: \"paste\",\n bytes: joinPasteBytes(paste.parts, paste.totalLength),\n })\n\n this.paste = null\n return combined.subarray(endIndex + BRACKETED_PASTE_END.length)\n }\n\n // Keep enough trailing bytes to detect an end marker split across chunks.\n // Everything before that point is safe to retain immediately.\n const keep = Math.min(BRACKETED_PASTE_END.length - 1, combined.length)\n const stableLength = combined.length - keep\n if (stableLength > 0) {\n this.pushPasteBytes(combined.subarray(0, stableLength))\n }\n\n paste.tail = Uint8Array.from(combined.subarray(stableLength))\n return EMPTY_BYTES\n }\n\n private pushPasteBytes(bytes: Uint8Array): void {\n if (bytes.length === 0) {\n return\n }\n\n // Copy here because subarray() inputs may alias the caller's chunk or the\n // parser's pending buffer across pushes. The emitted paste event must keep\n // the original bytes even if those backing buffers are later reused.\n this.paste!.parts.push(Uint8Array.from(bytes))\n this.paste!.totalLength += bytes.length\n }\n\n private reconcileDeferredStateWithProtocolContext(): void {\n switch (this.state.tag) {\n case \"csi_parametric_deferred\":\n if (!canDeferParametricCsi(this.state, this.protocolContext)) {\n this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n }\n return\n\n case \"csi_private_reply_deferred\":\n if (!canDeferPrivateReplyCsi(this.protocolContext)) {\n this.emitOpaqueResponse(\"unknown\", this.pending.view().subarray(this.unitStart, this.cursor))\n this.state = { tag: \"ground\" }\n this.consumePrefix(this.cursor)\n }\n return\n }\n }\n\n // Arms or disarms the timeout after every push(). If there's an incomplete\n // unit in the buffer, starts a timer. When the timer fires, it sets\n // forceFlush so the next read() converts the incomplete unit into one\n // atomic event (e.g. a lone ESC becoming an Escape key).\n private reconcileTimeoutState(): void {\n if (!this.armTimeouts) {\n return\n }\n\n if (this.paste || this.pendingSinceMs === null || this.pending.length === 0) {\n this.clearTimeout()\n return\n }\n\n this.clearTimeout()\n this.timeoutId = this.clock.setTimeout(() => {\n this.timeoutId = null\n if (this.destroyed) {\n return\n }\n\n try {\n this.flushTimeout(this.clock.now())\n this.onTimeoutFlush?.()\n } catch (error) {\n console.error(\"stdin parser timeout flush failed\", error)\n }\n }, this.timeoutMs)\n }\n\n private clearTimeout(): void {\n if (!this.timeoutId) {\n return\n }\n\n this.clock.clearTimeout(this.timeoutId)\n this.timeoutId = null\n }\n\n // Clears all parser state: pending bytes, queued events, timeout tracking,\n // and any active paste collector. Called by both reset() (suspend/resume)\n // and destroy() to ensure no stale state survives.\n private resetState(): void {\n this.pending.reset(INITIAL_PENDING_CAPACITY)\n this.events.length = 0\n this.pendingSinceMs = null\n this.forceFlush = false\n this.justFlushedEsc = false\n this.state = { tag: \"ground\" }\n this.cursor = 0\n this.unitStart = 0\n this.paste = null\n this.mouseParser.reset()\n }\n}\n",
27
27
  "import type { TextChunk } from \"../text-buffer.js\"\nimport { StyledText } from \"./styled-text.js\"\nimport { SyntaxStyle, type StyleDefinition } from \"../syntax-style.js\"\nimport { TreeSitterClient } from \"./tree-sitter/client.js\"\nimport type { SimpleHighlight } from \"./tree-sitter/types.js\"\nimport { createTextAttributes } from \"../utils.js\"\nimport { registerEnvVar, env } from \"./env.js\"\n\nregisterEnvVar({ name: \"OTUI_TS_STYLE_WARN\", default: false, description: \"Enable warnings for missing syntax styles\" })\n\ninterface ConcealOptions {\n enabled: boolean\n}\n\ninterface Boundary {\n offset: number\n type: \"start\" | \"end\"\n highlightIndex: number\n}\n\nfunction getSpecificity(group: string): number {\n return group.split(\".\").length\n}\n\nfunction shouldSuppressInInjection(group: string, meta: any): boolean {\n if (meta?.isInjection) {\n return false\n }\n\n // Check if this is a parent block that should be suppressed\n // TODO: This is language/highlight specific,\n // not generic enough. Needs a more generic solution.\n // The styles need to be more like a stack that gets merged\n // and for a container with injections we just don't push that container style\n return group === \"markup.raw.block\"\n}\n\nexport function treeSitterToTextChunks(\n content: string,\n highlights: SimpleHighlight[],\n syntaxStyle: SyntaxStyle,\n options?: ConcealOptions,\n): TextChunk[] {\n const chunks: TextChunk[] = []\n const defaultStyle = syntaxStyle.getStyle(\"default\")\n const concealEnabled = options?.enabled ?? true\n\n const injectionContainerRanges: Array<{ start: number; end: number }> = []\n const boundaries: Boundary[] = []\n\n for (let i = 0; i < highlights.length; i++) {\n const [start, end, , meta] = highlights[i]\n if (start === end) continue // Skip zero-length ranges\n if (meta?.containsInjection) {\n injectionContainerRanges.push({ start, end })\n }\n boundaries.push({ offset: start, type: \"start\", highlightIndex: i })\n boundaries.push({ offset: end, type: \"end\", highlightIndex: i })\n }\n\n // Sort boundaries by offset, with ends before starts at same offset\n // This ensures we close old ranges before opening new ones at the same position\n boundaries.sort((a, b) => {\n if (a.offset !== b.offset) return a.offset - b.offset\n if (a.type === \"end\" && b.type === \"start\") return -1\n if (a.type === \"start\" && b.type === \"end\") return 1\n return 0\n })\n\n const activeHighlights = new Set<number>()\n let currentOffset = 0\n\n for (let i = 0; i < boundaries.length; i++) {\n const boundary = boundaries[i]\n\n if (currentOffset < boundary.offset && activeHighlights.size > 0) {\n const segmentText = content.slice(currentOffset, boundary.offset)\n\n const activeGroups: Array<{ group: string; meta: any; index: number }> = []\n for (const idx of activeHighlights) {\n const [, , group, meta] = highlights[idx]\n activeGroups.push({ group, meta, index: idx })\n }\n\n // Check if any active highlight has a conceal property\n // Priority: 1. Check meta.conceal first 2. Check group === \"conceal\" or starts with \"conceal.\"\n const concealHighlight = concealEnabled\n ? activeGroups.find(\n (h) => h.meta?.conceal !== undefined || h.group === \"conceal\" || h.group.startsWith(\"conceal.\"),\n )\n : undefined\n\n if (concealHighlight) {\n let replacementText = \"\"\n\n if (concealHighlight.meta?.conceal !== undefined) {\n // If meta.conceal is set, use it (this would come from (#set! conceal \"...\") if supported)\n replacementText = concealHighlight.meta.conceal\n } else if (concealHighlight.group === \"conceal.with.space\") {\n // Special group name means replace with space\n replacementText = \" \"\n }\n\n if (replacementText) {\n chunks.push({\n __isChunk: true,\n text: replacementText,\n fg: defaultStyle?.fg,\n bg: defaultStyle?.bg,\n attributes: defaultStyle\n ? createTextAttributes({\n bold: defaultStyle.bold,\n italic: defaultStyle.italic,\n underline: defaultStyle.underline,\n dim: defaultStyle.dim,\n })\n : 0,\n })\n }\n } else {\n const insideInjectionContainer = injectionContainerRanges.some(\n (range) => currentOffset >= range.start && currentOffset < range.end,\n )\n\n // Filter out highlights that should be suppressed\n // Suppress highlights when we're inside an injection container\n const validGroups = activeGroups.filter((h) => {\n // If we're inside an injection container, suppress all markup.raw.block highlights\n // This includes both the container itself and any nested markup.raw.block\n if (insideInjectionContainer && shouldSuppressInInjection(h.group, h.meta)) {\n return false\n }\n return true\n })\n\n // Sort groups by specificity (least to most), then by index (earlier to later)\n // This ensures we merge styles in the correct order: parent styles first, then child overrides\n const sortedGroups = validGroups.sort((a, b) => {\n const aSpec = getSpecificity(a.group)\n const bSpec = getSpecificity(b.group)\n if (aSpec !== bSpec) return aSpec - bSpec // Lower specificity first\n return a.index - b.index // Earlier index first\n })\n\n // Merge all active styles in order (like CSS cascade)\n // Later/more specific styles override earlier/less specific ones\n const mergedStyle: StyleDefinition = {}\n\n for (const { group } of sortedGroups) {\n let styleForGroup = syntaxStyle.getStyle(group)\n\n if (!styleForGroup && group.includes(\".\")) {\n // Fallback to base scope\n const baseName = group.split(\".\")[0]\n styleForGroup = syntaxStyle.getStyle(baseName)\n }\n\n if (styleForGroup) {\n // Merge properties - later styles override earlier ones\n if (styleForGroup.fg !== undefined) mergedStyle.fg = styleForGroup.fg\n if (styleForGroup.bg !== undefined) mergedStyle.bg = styleForGroup.bg\n if (styleForGroup.bold !== undefined) mergedStyle.bold = styleForGroup.bold\n if (styleForGroup.italic !== undefined) mergedStyle.italic = styleForGroup.italic\n if (styleForGroup.underline !== undefined) mergedStyle.underline = styleForGroup.underline\n if (styleForGroup.dim !== undefined) mergedStyle.dim = styleForGroup.dim\n } else {\n if (group.includes(\".\")) {\n const baseName = group.split(\".\")[0]\n if (env.OTUI_TS_STYLE_WARN) {\n console.warn(\n `Syntax style not found for group \"${group}\" or base scope \"${baseName}\", using default style`,\n )\n }\n } else {\n if (env.OTUI_TS_STYLE_WARN) {\n console.warn(`Syntax style not found for group \"${group}\", using default style`)\n }\n }\n }\n }\n\n // Use merged style, falling back to default if nothing was merged\n const finalStyle = Object.keys(mergedStyle).length > 0 ? mergedStyle : defaultStyle\n\n chunks.push({\n __isChunk: true,\n text: segmentText,\n fg: finalStyle?.fg,\n bg: finalStyle?.bg,\n attributes: finalStyle\n ? createTextAttributes({\n bold: finalStyle.bold,\n italic: finalStyle.italic,\n underline: finalStyle.underline,\n dim: finalStyle.dim,\n })\n : 0,\n })\n }\n } else if (currentOffset < boundary.offset) {\n const text = content.slice(currentOffset, boundary.offset)\n chunks.push({\n __isChunk: true,\n text,\n fg: defaultStyle?.fg,\n bg: defaultStyle?.bg,\n attributes: defaultStyle\n ? createTextAttributes({\n bold: defaultStyle.bold,\n italic: defaultStyle.italic,\n underline: defaultStyle.underline,\n dim: defaultStyle.dim,\n })\n : 0,\n })\n }\n\n if (boundary.type === \"start\") {\n activeHighlights.add(boundary.highlightIndex)\n } else {\n activeHighlights.delete(boundary.highlightIndex)\n\n if (concealEnabled) {\n const [, , group, meta] = highlights[boundary.highlightIndex]\n if (meta?.concealLines !== undefined) {\n if (boundary.offset < content.length && content[boundary.offset] === \"\\n\") {\n currentOffset = boundary.offset + 1\n continue\n }\n }\n\n // TODO: This is also a query specific workaround, needs improvement\n if (meta?.conceal !== undefined) {\n // Skip the next space if we replaced with a space (prevents double spaces like \"text] (url)\")\n if (meta.conceal === \" \") {\n if (boundary.offset < content.length && content[boundary.offset] === \" \") {\n currentOffset = boundary.offset + 1\n continue\n }\n }\n // For heading markers specifically, also skip the trailing space\n // The group is just \"conceal\" for heading markers from the markdown query\n // We need to check if this conceal is NOT from an injection (markdown_inline)\n else if (meta.conceal === \"\" && group === \"conceal\" && !meta.isInjection) {\n if (boundary.offset < content.length && content[boundary.offset] === \" \") {\n currentOffset = boundary.offset + 1\n continue\n }\n }\n }\n }\n }\n\n currentOffset = boundary.offset\n }\n\n if (currentOffset < content.length) {\n const text = content.slice(currentOffset)\n chunks.push({\n __isChunk: true,\n text,\n fg: defaultStyle?.fg,\n bg: defaultStyle?.bg,\n attributes: defaultStyle\n ? createTextAttributes({\n bold: defaultStyle.bold,\n italic: defaultStyle.italic,\n underline: defaultStyle.underline,\n dim: defaultStyle.dim,\n })\n : 0,\n })\n }\n\n return chunks\n}\n\nexport interface TreeSitterToStyledTextOptions {\n conceal?: ConcealOptions\n}\n\nexport async function treeSitterToStyledText(\n content: string,\n filetype: string,\n syntaxStyle: SyntaxStyle,\n client: TreeSitterClient,\n options?: TreeSitterToStyledTextOptions,\n): Promise<StyledText> {\n const result = await client.highlightOnce(content, filetype)\n if (result.highlights && result.highlights.length > 0) {\n const chunks = treeSitterToTextChunks(content, result.highlights, syntaxStyle, options?.conceal)\n return new StyledText(chunks)\n } else {\n const defaultStyle = syntaxStyle.mergeStyles(\"default\")\n const chunks: TextChunk[] = [\n {\n __isChunk: true,\n text: content,\n fg: defaultStyle.fg,\n bg: defaultStyle.bg,\n attributes: defaultStyle.attributes,\n },\n ]\n return new StyledText(chunks)\n }\n}\n",
28
28
  "import { EventEmitter } from \"events\"\nimport { createDebounce, clearDebounceScope, DebounceController } from \"../debounce.js\"\nimport { ProcessQueue } from \"../queue.js\"\nimport type {\n TreeSitterClientOptions,\n TreeSitterClientEvents,\n BufferState,\n ParsedBuffer,\n FiletypeParserOptions,\n Edit,\n PerformanceStats,\n SimpleHighlight,\n} from \"./types.js\"\nimport { getParsers } from \"./default-parsers.js\"\nimport { resolve, isAbsolute, parse } from \"path\"\nimport { existsSync } from \"fs\"\nimport { registerEnvVar, env } from \"../env.js\"\nimport { isBunfsPath, normalizeBunfsPath } from \"../bunfs.js\"\n\nregisterEnvVar({\n name: \"OTUI_TREE_SITTER_WORKER_PATH\",\n description: \"Path to the TreeSitter worker\",\n type: \"string\",\n default: \"\",\n})\n\ndeclare global {\n const OTUI_TREE_SITTER_WORKER_PATH: string\n}\n\ninterface EditQueueItem {\n edits: Edit[]\n newContent: string\n version: number\n isReset?: boolean\n}\n\nlet DEFAULT_PARSERS: FiletypeParserOptions[] = getParsers()\n\nexport function addDefaultParsers(parsers: FiletypeParserOptions[]): void {\n for (const parser of parsers) {\n DEFAULT_PARSERS = [\n ...DEFAULT_PARSERS.filter((existingParser) => existingParser.filetype !== parser.filetype),\n parser,\n ]\n }\n}\n\nconst isUrl = (path: string) => path.startsWith(\"http://\") || path.startsWith(\"https://\")\n\n// Parser options now support both URLs and local file paths\n// TODO: TreeSitterClient should have a setOptions method, passing it on to the worker etc.\nexport class TreeSitterClient extends EventEmitter<TreeSitterClientEvents> {\n private initialized = false\n private worker: Worker | undefined\n private buffers: Map<number, BufferState> = new Map()\n private initializePromise: Promise<void> | undefined\n private initializeResolvers:\n | { resolve: () => void; reject: (error: Error) => void; timeoutId: ReturnType<typeof setTimeout> }\n | undefined\n private messageCallbacks: Map<string, (response: any) => void> = new Map()\n private messageIdCounter: number = 0\n private editQueues: Map<number, ProcessQueue<EditQueueItem>> = new Map()\n private debouncer: DebounceController\n private options: TreeSitterClientOptions\n\n constructor(options: TreeSitterClientOptions) {\n super()\n this.options = options\n this.debouncer = createDebounce(\"tree-sitter-client\")\n this.startWorker()\n }\n\n private emitError(error: string, bufferId?: number): void {\n if (this.listenerCount(\"error\") > 0) {\n this.emit(\"error\", error, bufferId)\n }\n }\n\n private emitWarning(warning: string, bufferId?: number): void {\n if (this.listenerCount(\"warning\") > 0) {\n this.emit(\"warning\", warning, bufferId)\n }\n }\n\n private startWorker() {\n if (this.worker) {\n return\n }\n\n let worker_path: string | URL\n\n if (env.OTUI_TREE_SITTER_WORKER_PATH) {\n worker_path = env.OTUI_TREE_SITTER_WORKER_PATH\n } else if (typeof OTUI_TREE_SITTER_WORKER_PATH !== \"undefined\") {\n worker_path = OTUI_TREE_SITTER_WORKER_PATH\n } else if (this.options.workerPath) {\n worker_path = this.options.workerPath\n } else {\n worker_path = new URL(\"./parser.worker.js\", import.meta.url).href\n if (!existsSync(resolve(import.meta.dirname, \"parser.worker.js\"))) {\n worker_path = new URL(\"./parser.worker.ts\", import.meta.url).href\n }\n }\n\n this.worker = new Worker(worker_path)\n\n // @ts-ignore - onmessage exists\n this.worker.onmessage = this.handleWorkerMessage.bind(this)\n\n // @ts-ignore - onerror exists\n this.worker.onerror = (error: ErrorEvent) => {\n console.error(\"TreeSitter worker error:\", error.message)\n\n // If we're still initializing, reject the init promise\n if (this.initializeResolvers) {\n clearTimeout(this.initializeResolvers.timeoutId)\n this.initializeResolvers.reject(new Error(`Worker error: ${error.message}`))\n this.initializeResolvers = undefined\n }\n\n this.emitError(`Worker error: ${error.message}`)\n }\n }\n\n private stopWorker() {\n if (!this.worker) {\n return\n }\n\n this.worker.terminate()\n this.worker = undefined\n }\n\n // NOTE: Unused, but useful for debugging and testing\n private handleReset() {\n this.buffers.clear()\n this.stopWorker()\n this.startWorker()\n this.initializePromise = undefined\n this.initializeResolvers = undefined\n return this.initialize()\n }\n\n async initialize(): Promise<void> {\n if (this.initializePromise) {\n return this.initializePromise\n }\n\n this.initializePromise = new Promise((resolve, reject) => {\n const timeoutMs = this.options.initTimeout ?? 10000 // Default to 10 seconds\n const timeoutId = setTimeout(() => {\n const error = new Error(\"Worker initialization timed out\")\n console.error(\"TreeSitter client:\", error.message)\n this.initializeResolvers = undefined\n reject(error)\n }, timeoutMs)\n\n this.initializeResolvers = { resolve, reject, timeoutId }\n this.worker?.postMessage({\n type: \"INIT\",\n dataPath: this.options.dataPath,\n })\n })\n\n await this.initializePromise\n await this.registerDefaultParsers()\n\n return this.initializePromise\n }\n\n private async registerDefaultParsers(): Promise<void> {\n for (const parser of DEFAULT_PARSERS) {\n this.addFiletypeParser(parser)\n }\n }\n\n private resolvePath(path: string): string {\n if (isUrl(path)) {\n return path\n }\n if (isBunfsPath(path)) {\n return normalizeBunfsPath(parse(path).base)\n }\n if (!isAbsolute(path)) {\n return resolve(path)\n }\n return path\n }\n\n public addFiletypeParser(filetypeParser: FiletypeParserOptions): void {\n const resolvedParser: FiletypeParserOptions = {\n ...filetypeParser,\n aliases: filetypeParser.aliases\n ? [...new Set(filetypeParser.aliases.filter((alias) => alias !== filetypeParser.filetype))]\n : undefined,\n wasm: this.resolvePath(filetypeParser.wasm),\n queries: {\n highlights: filetypeParser.queries.highlights.map((path) => this.resolvePath(path)),\n injections: filetypeParser.queries.injections?.map((path) => this.resolvePath(path)),\n },\n }\n this.worker?.postMessage({ type: \"ADD_FILETYPE_PARSER\", filetypeParser: resolvedParser })\n }\n\n public async getPerformance(): Promise<PerformanceStats> {\n const messageId = `performance_${this.messageIdCounter++}`\n return new Promise<PerformanceStats>((resolve) => {\n this.messageCallbacks.set(messageId, resolve)\n this.worker?.postMessage({ type: \"GET_PERFORMANCE\", messageId })\n })\n }\n\n public async highlightOnce(\n content: string,\n filetype: string,\n ): Promise<{ highlights?: SimpleHighlight[]; warning?: string; error?: string }> {\n if (!this.initialized) {\n try {\n await this.initialize()\n } catch (error) {\n return { error: \"Could not highlight because of initialization error\" }\n }\n }\n\n const messageId = `oneshot_${this.messageIdCounter++}`\n return new Promise((resolve) => {\n this.messageCallbacks.set(messageId, resolve)\n this.worker?.postMessage({\n type: \"ONESHOT_HIGHLIGHT\",\n content,\n filetype,\n messageId,\n })\n })\n }\n\n private handleWorkerMessage(event: MessageEvent) {\n const { type, bufferId, error, highlights, warning, messageId, hasParser, performance, version } = event.data\n\n if (type === \"HIGHLIGHT_RESPONSE\") {\n const buffer = this.buffers.get(bufferId)\n if (!buffer || !buffer.hasParser) return\n if (buffer.version !== version) {\n this.resetBuffer(bufferId, buffer.version, buffer.content)\n return\n }\n this.emit(\"highlights:response\", bufferId, version, highlights)\n }\n\n if (type === \"INIT_RESPONSE\") {\n if (this.initializeResolvers) {\n clearTimeout(this.initializeResolvers.timeoutId)\n if (error) {\n console.error(\"TreeSitter client initialization failed:\", error)\n this.initializeResolvers.reject(new Error(error))\n } else {\n this.initialized = true\n this.initializeResolvers.resolve()\n }\n this.initializeResolvers = undefined\n return\n }\n }\n\n if (type === \"PARSER_INIT_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback({ hasParser, warning, error })\n }\n return\n }\n\n if (type === \"PRELOAD_PARSER_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback({ hasParser })\n }\n return\n }\n\n if (type === \"BUFFER_DISPOSED\") {\n const callback = this.messageCallbacks.get(`dispose_${bufferId}`)\n if (callback) {\n this.messageCallbacks.delete(`dispose_${bufferId}`)\n callback(true)\n }\n this.emit(\"buffer:disposed\", bufferId)\n return\n }\n\n if (type === \"PERFORMANCE_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback(performance)\n }\n return\n }\n\n if (type === \"ONESHOT_HIGHLIGHT_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback({ highlights, warning, error })\n }\n return\n }\n\n if (type === \"UPDATE_DATA_PATH_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback({ error })\n }\n return\n }\n\n if (type === \"CLEAR_CACHE_RESPONSE\") {\n const callback = this.messageCallbacks.get(messageId)\n if (callback) {\n this.messageCallbacks.delete(messageId)\n callback({ error })\n }\n return\n }\n\n if (warning) {\n this.emitWarning(warning, bufferId)\n return\n }\n\n if (error) {\n this.emitError(error, bufferId)\n return\n }\n\n if (type === \"WORKER_LOG\") {\n const { logType, data } = event.data\n const message = data.join(\" \")\n\n this.emit(\"worker:log\", logType, message)\n\n if (logType === \"log\") {\n console.log(\"TSWorker:\", ...data)\n } else if (logType === \"error\") {\n console.error(\"TSWorker:\", ...data)\n } else if (logType === \"warn\") {\n console.warn(\"TSWorker:\", ...data)\n }\n return\n }\n }\n\n public async preloadParser(filetype: string): Promise<boolean> {\n const messageId = `has_parser_${this.messageIdCounter++}`\n const response = await new Promise<{ hasParser: boolean; warning?: string; error?: string }>((resolve) => {\n this.messageCallbacks.set(messageId, resolve)\n this.worker?.postMessage({\n type: \"PRELOAD_PARSER\",\n filetype,\n messageId,\n })\n })\n return response.hasParser\n }\n\n public async createBuffer(\n id: number,\n content: string,\n filetype: string,\n version: number = 1,\n autoInitialize: boolean = true,\n ): Promise<boolean> {\n if (!this.initialized) {\n if (!autoInitialize) {\n this.emitError(\"Could not create buffer because client is not initialized\")\n return false\n }\n try {\n await this.initialize()\n } catch (error) {\n this.emitError(\"Could not create buffer because of initialization error\")\n return false\n }\n }\n\n if (this.buffers.has(id)) {\n throw new Error(`Buffer with id ${id} already exists`)\n }\n\n // Set buffer state immediately to avoid race conditions\n this.buffers.set(id, { id, content, filetype, version, hasParser: false })\n\n const messageId = `init_${this.messageIdCounter++}`\n const response = await new Promise<{ hasParser: boolean; warning?: string; error?: string }>((resolve) => {\n this.messageCallbacks.set(messageId, resolve)\n this.worker?.postMessage({\n type: \"INITIALIZE_PARSER\",\n bufferId: id,\n version,\n content,\n filetype,\n messageId,\n })\n })\n\n if (!response.hasParser) {\n this.emit(\"buffer:initialized\", id, false)\n if (filetype !== \"plaintext\") {\n this.emitWarning(response.warning || response.error || \"Buffer has no parser\", id)\n }\n return false\n }\n\n // Update buffer state to indicate it has a parser\n const bufferState: ParsedBuffer = { id, content, filetype, version, hasParser: true }\n this.buffers.set(id, bufferState)\n\n this.emit(\"buffer:initialized\", id, true)\n return true\n }\n\n public async updateBuffer(id: number, edits: Edit[], newContent: string, version: number): Promise<void> {\n if (!this.initialized) {\n return\n }\n\n const buffer = this.buffers.get(id)\n if (!buffer || !buffer.hasParser) {\n return\n }\n\n // Update buffer state\n this.buffers.set(id, { ...buffer, content: newContent, version })\n\n if (!this.editQueues.has(id)) {\n this.editQueues.set(\n id,\n new ProcessQueue<EditQueueItem>((item) =>\n this.processEdit(id, item.edits, item.newContent, item.version, item.isReset),\n ),\n )\n }\n\n const bufferQueue = this.editQueues.get(id)!\n bufferQueue.enqueue({ edits, newContent, version })\n }\n\n private async processEdit(\n bufferId: number,\n edits: Edit[],\n newContent: string,\n version: number,\n isReset = false,\n ): Promise<void> {\n this.worker?.postMessage({\n type: isReset ? \"RESET_BUFFER\" : \"HANDLE_EDITS\",\n bufferId,\n version,\n content: newContent,\n edits,\n })\n }\n\n public async removeBuffer(bufferId: number): Promise<void> {\n if (!this.initialized) {\n return\n }\n\n this.buffers.delete(bufferId)\n\n if (this.editQueues.has(bufferId)) {\n this.editQueues.get(bufferId)?.clear()\n this.editQueues.delete(bufferId)\n }\n\n if (this.worker) {\n await new Promise<boolean>((resolve) => {\n const messageId = `dispose_${bufferId}`\n this.messageCallbacks.set(messageId, resolve)\n try {\n this.worker!.postMessage({\n type: \"DISPOSE_BUFFER\",\n bufferId,\n })\n } catch (error) {\n console.error(\"Error disposing buffer\", error)\n resolve(false)\n }\n\n // Add a timeout in case the worker doesn't respond\n setTimeout(() => {\n if (this.messageCallbacks.has(messageId)) {\n this.messageCallbacks.delete(messageId)\n console.warn({ bufferId }, \"Timed out waiting for buffer to be disposed\")\n resolve(false)\n }\n }, 3000)\n })\n }\n\n this.debouncer.clearDebounce(`reset-${bufferId}`)\n }\n\n public async destroy(): Promise<void> {\n if (this.initializeResolvers) {\n clearTimeout(this.initializeResolvers.timeoutId)\n // Reject pending initialization promise to prevent hanging awaits\n this.initializeResolvers.reject(new Error(\"Client destroyed during initialization\"))\n this.initializeResolvers = undefined\n }\n\n for (const [messageId, callback] of this.messageCallbacks.entries()) {\n if (typeof callback === \"function\") {\n try {\n callback({ error: \"Client destroyed\" })\n } catch (e) {\n // Ignore errors during cleanup\n }\n }\n }\n this.messageCallbacks.clear()\n\n clearDebounceScope(\"tree-sitter-client\")\n this.debouncer.clear()\n\n this.editQueues.clear()\n this.buffers.clear()\n\n this.stopWorker()\n\n this.initialized = false\n this.initializePromise = undefined\n }\n\n public async resetBuffer(bufferId: number, version: number, content: string): Promise<void> {\n if (!this.initialized) {\n return\n }\n\n const buffer = this.buffers.get(bufferId)\n if (!buffer || !buffer.hasParser) {\n this.emitError(\"Cannot reset buffer with no parser\", bufferId)\n return\n }\n\n // Update buffer state\n this.buffers.set(bufferId, { ...buffer, content, version })\n\n // Use debouncer to avoid excessive resets\n this.debouncer.debounce(`reset-${bufferId}`, 10, () => this.processEdit(bufferId, [], content, version, true))\n }\n\n public getBuffer(bufferId: number): BufferState | undefined {\n return this.buffers.get(bufferId)\n }\n\n public getAllBuffers(): BufferState[] {\n return Array.from(this.buffers.values())\n }\n\n public isInitialized(): boolean {\n return this.initialized\n }\n\n public async setDataPath(dataPath: string): Promise<void> {\n if (this.options.dataPath === dataPath) {\n return\n }\n\n this.options.dataPath = dataPath\n\n if (this.initialized && this.worker) {\n const messageId = `update_datapath_${this.messageIdCounter++}`\n return new Promise<void>((resolve, reject) => {\n this.messageCallbacks.set(messageId, (response: any) => {\n if (response.error) {\n reject(new Error(response.error))\n } else {\n resolve()\n }\n })\n this.worker!.postMessage({\n type: \"UPDATE_DATA_PATH\",\n dataPath,\n messageId,\n })\n })\n }\n }\n\n public async clearCache(): Promise<void> {\n if (!this.initialized || !this.worker) {\n throw new Error(\"Cannot clear cache: client is not initialized\")\n }\n\n const messageId = `clear_cache_${this.messageIdCounter++}`\n return new Promise<void>((resolve, reject) => {\n this.messageCallbacks.set(messageId, (response: any) => {\n if (response.error) {\n reject(new Error(response.error))\n } else {\n resolve()\n }\n })\n this.worker!.postMessage({\n type: \"CLEAR_CACHE\",\n messageId,\n })\n })\n }\n}\n",
29
29
  "/**\n * A module-level map to store timeout IDs for all debounced functions\n * Structure: Map<scopeId, Map<debounceId, timerId>>\n */\nconst TIMERS_MAP = new Map<string | number, Map<string | number, ReturnType<typeof setTimeout>>>()\n\n/**\n * Debounce controller that manages debounce instances for a specific scope\n */\nexport class DebounceController {\n constructor(private scopeId: string | number) {\n // Initialize the scope map if it doesn't exist\n if (!TIMERS_MAP.has(this.scopeId)) {\n TIMERS_MAP.set(this.scopeId, new Map())\n }\n }\n\n /**\n * Debounces the provided function with the given ID\n *\n * @param id Unique identifier within this scope\n * @param ms Milliseconds to wait before executing\n * @param fn Function to execute\n */\n debounce<R>(id: string | number, ms: number, fn: () => Promise<R>): Promise<R> {\n const scopeMap = TIMERS_MAP.get(this.scopeId)!\n\n return new Promise((resolve, reject) => {\n // Clear any existing timeout for this ID\n if (scopeMap.has(id)) {\n clearTimeout(scopeMap.get(id))\n }\n\n // Set a new timeout\n const timerId = setTimeout(() => {\n try {\n resolve(fn())\n } catch (error) {\n reject(error)\n }\n scopeMap.delete(id)\n }, ms)\n\n // Store the new timeout ID\n scopeMap.set(id, timerId)\n })\n }\n\n /**\n * Clear a specific debounce timer in this scope\n *\n * @param id The debounce ID to clear\n */\n clearDebounce(id: string | number): void {\n const scopeMap = TIMERS_MAP.get(this.scopeId)\n if (scopeMap && scopeMap.has(id)) {\n clearTimeout(scopeMap.get(id))\n scopeMap.delete(id)\n }\n }\n\n /**\n * Clear all debounce timers in this scope\n */\n clear(): void {\n const scopeMap = TIMERS_MAP.get(this.scopeId)\n if (scopeMap) {\n scopeMap.forEach((timerId) => clearTimeout(timerId))\n scopeMap.clear()\n }\n }\n}\n\n/**\n * Creates a new debounce controller for a specific scope\n *\n * @param scopeId Unique identifier for this debounce scope\n * @returns A DebounceController for the specified scope\n */\nexport function createDebounce(scopeId: string | number): DebounceController {\n return new DebounceController(scopeId)\n}\n\n/**\n * Clears all debounce timers for a specific scope\n *\n * @param scopeId The scope identifier\n */\nexport function clearDebounceScope(scopeId: string | number): void {\n const scopeMap = TIMERS_MAP.get(scopeId)\n if (scopeMap) {\n scopeMap.forEach((timerId) => clearTimeout(timerId))\n scopeMap.clear()\n }\n}\n\n/**\n * Clears all active debounce timers across all scopes\n */\nexport function clearAllDebounces(): void {\n TIMERS_MAP.forEach((scopeMap) => {\n scopeMap.forEach((timerId) => clearTimeout(timerId))\n scopeMap.clear()\n })\n TIMERS_MAP.clear()\n}\n",
30
30
  "/**\n * Generic processing queue that handles asynchronous job processing\n */\nexport class ProcessQueue<T> {\n private queue: T[] = []\n private processing: boolean = false\n private autoProcess: boolean = true\n\n constructor(\n private processor: (item: T) => Promise<void> | void,\n autoProcess: boolean = true,\n ) {\n this.autoProcess = autoProcess\n }\n\n enqueue(item: T): void {\n this.queue.push(item)\n\n if (!this.processing && this.autoProcess) {\n this.processQueue()\n }\n }\n\n private processQueue(): void {\n if (this.queue.length === 0) {\n return\n }\n\n this.processing = true\n\n queueMicrotask(async () => {\n if (this.queue.length === 0) {\n this.processing = false\n return\n }\n\n // Get the next item to process (FIFO)\n const item = this.queue.shift()!\n\n try {\n await this.processor(item)\n } catch (error) {\n console.error(\"Error processing queue item:\", error)\n }\n\n if (this.queue.length > 0) {\n this.processQueue()\n } else {\n this.processing = false\n }\n })\n }\n\n clear(): void {\n this.queue = []\n }\n\n isProcessing(): boolean {\n return this.processing\n }\n\n size(): number {\n return this.queue.length\n }\n}\n",
31
- "// This file is generated by assets/update.ts - DO NOT EDIT MANUALLY\n// Run 'bun assets/update.ts' to regenerate this file\n// Last generated: 2026-03-20T21:07:24.696Z\n\nimport type { FiletypeParserOptions } from \"./types\"\nimport { resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\n\nimport javascript_highlights from \"./assets/javascript/highlights.scm\" with { type: \"file\" }\nimport javascript_language from \"./assets/javascript/tree-sitter-javascript.wasm\" with { type: \"file\" }\nimport typescript_highlights from \"./assets/typescript/highlights.scm\" with { type: \"file\" }\nimport typescript_language from \"./assets/typescript/tree-sitter-typescript.wasm\" with { type: \"file\" }\nimport markdown_highlights from \"./assets/markdown/highlights.scm\" with { type: \"file\" }\nimport markdown_language from \"./assets/markdown/tree-sitter-markdown.wasm\" with { type: \"file\" }\nimport markdown_injections from \"./assets/markdown/injections.scm\" with { type: \"file\" }\nimport markdown_inline_highlights from \"./assets/markdown_inline/highlights.scm\" with { type: \"file\" }\nimport markdown_inline_language from \"./assets/markdown_inline/tree-sitter-markdown_inline.wasm\" with { type: \"file\" }\nimport zig_highlights from \"./assets/zig/highlights.scm\" with { type: \"file\" }\nimport zig_language from \"./assets/zig/tree-sitter-zig.wasm\" with { type: \"file\" }\n\n// Cached parsers to avoid re-resolving paths on every call\nlet _cachedParsers: FiletypeParserOptions[] | undefined\n\nexport function getParsers(): FiletypeParserOptions[] {\n if (!_cachedParsers) {\n _cachedParsers = [\n {\n filetype: \"javascript\",\n aliases: [\"javascriptreact\"],\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), javascript_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), javascript_language),\n },\n {\n filetype: \"typescript\",\n aliases: [\"typescriptreact\"],\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), typescript_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), typescript_language),\n },\n {\n filetype: \"markdown\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_highlights)],\n injections: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_injections)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_language),\n injectionMapping: {\n \"nodeTypes\": {\n \"inline\": \"markdown_inline\",\n \"pipe_table_cell\": \"markdown_inline\"\n },\n \"infoStringMap\": {\n \"javascript\": \"javascript\",\n \"js\": \"javascript\",\n \"jsx\": \"javascriptreact\",\n \"javascriptreact\": \"javascriptreact\",\n \"typescript\": \"typescript\",\n \"ts\": \"typescript\",\n \"tsx\": \"typescriptreact\",\n \"typescriptreact\": \"typescriptreact\",\n \"markdown\": \"markdown\",\n \"md\": \"markdown\"\n }\n},\n },\n {\n filetype: \"markdown_inline\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_language),\n },\n {\n filetype: \"zig\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), zig_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), zig_language),\n },\n ]\n }\n return _cachedParsers\n}\n",
31
+ "// This file is generated by assets/update.ts - DO NOT EDIT MANUALLY\n// Run 'bun assets/update.ts' to regenerate this file\n// Last generated: 2026-03-20T21:07:24.696Z\n\nimport type { FiletypeParserOptions } from \"./types.js\"\nimport { resolve, dirname } from \"path\"\nimport { fileURLToPath } from \"url\"\n\nimport javascript_highlights from \"./assets/javascript/highlights.scm\" with { type: \"file\" }\nimport javascript_language from \"./assets/javascript/tree-sitter-javascript.wasm\" with { type: \"file\" }\nimport typescript_highlights from \"./assets/typescript/highlights.scm\" with { type: \"file\" }\nimport typescript_language from \"./assets/typescript/tree-sitter-typescript.wasm\" with { type: \"file\" }\nimport markdown_highlights from \"./assets/markdown/highlights.scm\" with { type: \"file\" }\nimport markdown_language from \"./assets/markdown/tree-sitter-markdown.wasm\" with { type: \"file\" }\nimport markdown_injections from \"./assets/markdown/injections.scm\" with { type: \"file\" }\nimport markdown_inline_highlights from \"./assets/markdown_inline/highlights.scm\" with { type: \"file\" }\nimport markdown_inline_language from \"./assets/markdown_inline/tree-sitter-markdown_inline.wasm\" with { type: \"file\" }\nimport zig_highlights from \"./assets/zig/highlights.scm\" with { type: \"file\" }\nimport zig_language from \"./assets/zig/tree-sitter-zig.wasm\" with { type: \"file\" }\n\n// Cached parsers to avoid re-resolving paths on every call\nlet _cachedParsers: FiletypeParserOptions[] | undefined\n\nexport function getParsers(): FiletypeParserOptions[] {\n if (!_cachedParsers) {\n _cachedParsers = [\n {\n filetype: \"javascript\",\n aliases: [\"javascriptreact\"],\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), javascript_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), javascript_language),\n },\n {\n filetype: \"typescript\",\n aliases: [\"typescriptreact\"],\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), typescript_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), typescript_language),\n },\n {\n filetype: \"markdown\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_highlights)],\n injections: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_injections)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_language),\n injectionMapping: {\n \"nodeTypes\": {\n \"inline\": \"markdown_inline\",\n \"pipe_table_cell\": \"markdown_inline\"\n },\n \"infoStringMap\": {\n \"javascript\": \"javascript\",\n \"js\": \"javascript\",\n \"jsx\": \"javascriptreact\",\n \"javascriptreact\": \"javascriptreact\",\n \"typescript\": \"typescript\",\n \"ts\": \"typescript\",\n \"tsx\": \"typescriptreact\",\n \"typescriptreact\": \"typescriptreact\",\n \"markdown\": \"markdown\",\n \"md\": \"markdown\"\n }\n},\n },\n {\n filetype: \"markdown_inline\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), markdown_inline_language),\n },\n {\n filetype: \"zig\",\n queries: {\n highlights: [resolve(dirname(fileURLToPath(import.meta.url)), zig_highlights)],\n },\n wasm: resolve(dirname(fileURLToPath(import.meta.url)), zig_language),\n },\n ]\n }\n return _cachedParsers\n}\n",
32
32
  "import { basename, join } from \"node:path\"\n\nexport function isBunfsPath(path: string): boolean {\n // Removed ambiguous '//' check\n return path.includes(\"$bunfs\") || /^B:[\\\\/]~BUN/i.test(path)\n}\n\nexport function getBunfsRootPath(): string {\n return process.platform === \"win32\" ? \"B:\\\\~BUN\\\\root\" : \"/$bunfs/root\"\n}\n\n/**\n * Normalizes a path to the embedded root.\n * Flattens directory structure to ensure file exists at root.\n */\nexport function normalizeBunfsPath(fileName: string): string {\n return join(getBunfsRootPath(), basename(fileName))\n}\n",
33
33
  "import os from \"os\"\nimport path from \"path\"\nimport { EventEmitter } from \"events\"\nimport { singleton } from \"./singleton.js\"\nimport { env, registerEnvVar } from \"./env.js\"\nimport { isValidDirectoryName } from \"./validate-dir-name.js\"\n\n// Register environment variables for XDG directories\nregisterEnvVar({\n name: \"XDG_CONFIG_HOME\",\n description: \"Base directory for user-specific configuration files\",\n type: \"string\",\n default: \"\",\n})\n\nregisterEnvVar({\n name: \"XDG_DATA_HOME\",\n description: \"Base directory for user-specific data files\",\n type: \"string\",\n default: \"\",\n})\n\nexport interface DataPaths {\n globalConfigPath: string\n globalConfigFile: string\n localConfigFile: string\n globalDataPath: string\n}\n\nexport interface DataPathsEvents {\n \"paths:changed\": [paths: DataPaths]\n}\n\nexport class DataPathsManager extends EventEmitter<DataPathsEvents> {\n private _appName: string\n private _globalConfigPath?: string\n private _globalConfigFile?: string\n private _localConfigFile?: string\n private _globalDataPath?: string\n constructor() {\n super()\n this._appName = \"opentui\"\n }\n\n get appName(): string {\n return this._appName\n }\n\n set appName(value: string) {\n if (!isValidDirectoryName(value)) {\n throw new Error(`Invalid app name \"${value}\": must be a valid directory name`)\n }\n if (this._appName !== value) {\n this._appName = value\n this._globalConfigPath = undefined\n this._globalConfigFile = undefined\n this._localConfigFile = undefined\n this._globalDataPath = undefined\n this.emit(\"paths:changed\", this.toObject())\n }\n }\n\n get globalConfigPath(): string {\n if (this._globalConfigPath === undefined) {\n const homeDir = os.homedir()\n const xdgConfigHome = env.XDG_CONFIG_HOME\n const baseConfigDir = xdgConfigHome || path.join(homeDir, \".config\")\n this._globalConfigPath = path.join(baseConfigDir, this._appName)\n }\n return this._globalConfigPath\n }\n\n get globalConfigFile(): string {\n if (this._globalConfigFile === undefined) {\n this._globalConfigFile = path.join(this.globalConfigPath, \"init.ts\")\n }\n return this._globalConfigFile\n }\n\n get localConfigFile(): string {\n if (this._localConfigFile === undefined) {\n this._localConfigFile = path.join(process.cwd(), `.${this._appName}.ts`)\n }\n return this._localConfigFile\n }\n\n get globalDataPath(): string {\n if (this._globalDataPath === undefined) {\n const homeDir = os.homedir()\n const xdgDataHome = env.XDG_DATA_HOME\n const baseDataDir = xdgDataHome || path.join(homeDir, \".local/share\")\n this._globalDataPath = path.join(baseDataDir, this._appName)\n }\n return this._globalDataPath\n }\n\n toObject(): DataPaths {\n return {\n globalConfigPath: this.globalConfigPath,\n globalConfigFile: this.globalConfigFile,\n localConfigFile: this.localConfigFile,\n globalDataPath: this.globalDataPath,\n }\n }\n}\n\nexport function getDataPaths(): DataPathsManager {\n return singleton(\"data-paths-opentui\", () => new DataPathsManager())\n}\n",
34
34
  "export function isValidDirectoryName(name: string): boolean {\n if (!name || typeof name !== \"string\") {\n return false\n }\n\n if (name.trim().length === 0) {\n return false\n }\n\n const reservedNames = [\n \"CON\",\n \"PRN\",\n \"AUX\",\n \"NUL\",\n \"COM1\",\n \"COM2\",\n \"COM3\",\n \"COM4\",\n \"COM5\",\n \"COM6\",\n \"COM7\",\n \"COM8\",\n \"COM9\",\n \"LPT1\",\n \"LPT2\",\n \"LPT3\",\n \"LPT4\",\n \"LPT5\",\n \"LPT6\",\n \"LPT7\",\n \"LPT8\",\n \"LPT9\",\n ]\n if (reservedNames.includes(name.toUpperCase())) {\n return false\n }\n\n // Check for invalid characters\n // Windows: < > : \" | ? * \\ and control characters (0-31)\n // Unix: null character and forward slash\n const invalidChars = /[<>:\"|?*/\\\\\\x00-\\x1f]/\n if (invalidChars.test(name)) {\n return false\n }\n\n if (name.endsWith(\".\") || name.endsWith(\" \")) {\n return false\n }\n\n if (name === \".\" || name === \"..\") {\n return false\n }\n\n return true\n}\n",
@@ -38,11 +38,11 @@
38
38
  "import { singleton } from \"../singleton.js\"\nimport { TreeSitterClient } from \"./client.js\"\nimport type { TreeSitterClientOptions } from \"./types.js\"\nimport { getDataPaths } from \"../data-paths.js\"\n\nexport * from \"./client.js\"\nexport * from \"../tree-sitter-styled-text.js\"\nexport * from \"./types.js\"\nexport * from \"./resolve-ft.js\"\nexport type { UpdateOptions } from \"./assets/update.js\"\nexport { updateAssets } from \"./assets/update.js\"\n\nexport function getTreeSitterClient(): TreeSitterClient {\n const dataPathsManager = getDataPaths()\n const defaultOptions: TreeSitterClientOptions = {\n dataPath: dataPathsManager.globalDataPath,\n }\n\n return singleton(\"tree-sitter-client\", () => {\n const client = new TreeSitterClient(defaultOptions)\n\n dataPathsManager.on(\"paths:changed\", (paths) => {\n client.setDataPath(paths.globalDataPath)\n })\n\n return client\n })\n}\n",
39
39
  "import type { Extmark } from \"./extmarks.js\"\n\nexport interface ExtmarksSnapshot {\n extmarks: Map<number, Extmark>\n nextId: number\n}\n\nexport class ExtmarksHistory {\n private undoStack: ExtmarksSnapshot[] = []\n private redoStack: ExtmarksSnapshot[] = []\n\n saveSnapshot(extmarks: Map<number, Extmark>, nextId: number): void {\n const snapshot: ExtmarksSnapshot = {\n extmarks: new Map(Array.from(extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n nextId,\n }\n this.undoStack.push(snapshot)\n this.redoStack = []\n }\n\n undo(): ExtmarksSnapshot | null {\n if (this.undoStack.length === 0) return null\n return this.undoStack.pop()!\n }\n\n redo(): ExtmarksSnapshot | null {\n if (this.redoStack.length === 0) return null\n return this.redoStack.pop()!\n }\n\n pushRedo(snapshot: ExtmarksSnapshot): void {\n this.redoStack.push(snapshot)\n }\n\n pushUndo(snapshot: ExtmarksSnapshot): void {\n this.undoStack.push(snapshot)\n }\n\n clear(): void {\n this.undoStack = []\n this.redoStack = []\n }\n\n canUndo(): boolean {\n return this.undoStack.length > 0\n }\n\n canRedo(): boolean {\n return this.redoStack.length > 0\n }\n}\n",
40
40
  "import type { EditBuffer } from \"../edit-buffer.js\"\nimport type { EditorView } from \"../editor-view.js\"\nimport { ExtmarksHistory, type ExtmarksSnapshot } from \"./extmarks-history.js\"\n\nexport interface Extmark {\n id: number\n start: number // Display-width offset (including newlines), NOT JS string index\n end: number // Display-width offset (including newlines), NOT JS string index\n virtual: boolean\n styleId?: number\n priority?: number\n data?: any\n typeId: number\n}\n\nexport interface ExtmarkOptions {\n start: number // Display-width offset (including newlines), NOT JS string index\n end: number // Display-width offset (including newlines), NOT JS string index\n virtual?: boolean\n styleId?: number\n priority?: number\n data?: any\n typeId?: number\n metadata?: any\n}\n\n/**\n * WARNING: This is simulating extmarks in the edit buffer\n * and will move to a real native implementation in the future.\n * Use with caution.\n */\nexport class ExtmarksController {\n private editBuffer: EditBuffer\n private editorView: EditorView\n private extmarks = new Map<number, Extmark>()\n private extmarksByTypeId = new Map<number, Set<number>>()\n private metadata = new Map<number, any>()\n private nextId = 1\n private destroyed = false\n private history = new ExtmarksHistory()\n private typeNameToId = new Map<string, number>()\n private typeIdToName = new Map<number, string>()\n private nextTypeId = 1\n\n private originalMoveCursorLeft: typeof EditBuffer.prototype.moveCursorLeft\n private originalMoveCursorRight: typeof EditBuffer.prototype.moveCursorRight\n private originalSetCursorByOffset: typeof EditBuffer.prototype.setCursorByOffset\n private originalMoveUpVisual: typeof EditorView.prototype.moveUpVisual\n private originalMoveDownVisual: typeof EditorView.prototype.moveDownVisual\n private originalDeleteCharBackward: typeof EditBuffer.prototype.deleteCharBackward\n private originalDeleteChar: typeof EditBuffer.prototype.deleteChar\n private originalInsertText: typeof EditBuffer.prototype.insertText\n private originalInsertChar: typeof EditBuffer.prototype.insertChar\n private originalDeleteRange: typeof EditBuffer.prototype.deleteRange\n private originalSetText: typeof EditBuffer.prototype.setText\n private originalReplaceText: typeof EditBuffer.prototype.replaceText\n private originalClear: typeof EditBuffer.prototype.clear\n private originalNewLine: typeof EditBuffer.prototype.newLine\n private originalDeleteLine: typeof EditBuffer.prototype.deleteLine\n private originalEditorViewDeleteSelectedText: typeof EditorView.prototype.deleteSelectedText\n private originalUndo: typeof EditBuffer.prototype.undo\n private originalRedo: typeof EditBuffer.prototype.redo\n\n constructor(editBuffer: EditBuffer, editorView: EditorView) {\n this.editBuffer = editBuffer\n this.editorView = editorView\n\n this.originalMoveCursorLeft = editBuffer.moveCursorLeft.bind(editBuffer)\n this.originalMoveCursorRight = editBuffer.moveCursorRight.bind(editBuffer)\n this.originalSetCursorByOffset = editBuffer.setCursorByOffset.bind(editBuffer)\n this.originalMoveUpVisual = editorView.moveUpVisual.bind(editorView)\n this.originalMoveDownVisual = editorView.moveDownVisual.bind(editorView)\n this.originalDeleteCharBackward = editBuffer.deleteCharBackward.bind(editBuffer)\n this.originalDeleteChar = editBuffer.deleteChar.bind(editBuffer)\n this.originalInsertText = editBuffer.insertText.bind(editBuffer)\n this.originalInsertChar = editBuffer.insertChar.bind(editBuffer)\n this.originalDeleteRange = editBuffer.deleteRange.bind(editBuffer)\n this.originalSetText = editBuffer.setText.bind(editBuffer)\n this.originalReplaceText = editBuffer.replaceText.bind(editBuffer)\n this.originalClear = editBuffer.clear.bind(editBuffer)\n this.originalNewLine = editBuffer.newLine.bind(editBuffer)\n this.originalDeleteLine = editBuffer.deleteLine.bind(editBuffer)\n this.originalEditorViewDeleteSelectedText = editorView.deleteSelectedText.bind(editorView)\n this.originalUndo = editBuffer.undo.bind(editBuffer)\n this.originalRedo = editBuffer.redo.bind(editBuffer)\n\n this.wrapCursorMovement()\n this.wrapDeletion()\n this.wrapInsertion()\n this.wrapEditorViewDeleteSelectedText()\n this.wrapUndoRedo()\n this.setupContentChangeListener()\n }\n\n private wrapCursorMovement(): void {\n this.editBuffer.moveCursorLeft = (): void => {\n if (this.destroyed) {\n this.originalMoveCursorLeft()\n return\n }\n\n const currentOffset = this.editorView.getVisualCursor().offset\n const hasSelection = this.editorView.hasSelection()\n\n if (hasSelection) {\n this.originalMoveCursorLeft()\n return\n }\n\n const targetOffset = currentOffset - 1\n if (targetOffset < 0) {\n this.originalMoveCursorLeft()\n return\n }\n\n const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n if (virtualExtmark && currentOffset >= virtualExtmark.end) {\n this.editBuffer.setCursorByOffset(virtualExtmark.start - 1)\n return\n }\n\n this.originalMoveCursorLeft()\n }\n\n this.editBuffer.moveCursorRight = (): void => {\n if (this.destroyed) {\n this.originalMoveCursorRight()\n return\n }\n\n const currentOffset = this.editorView.getVisualCursor().offset\n const hasSelection = this.editorView.hasSelection()\n\n if (hasSelection) {\n this.originalMoveCursorRight()\n return\n }\n\n const targetOffset = currentOffset + 1\n const textLength = this.editBuffer.getText().length\n\n if (targetOffset > textLength) {\n this.originalMoveCursorRight()\n return\n }\n\n const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n if (virtualExtmark && currentOffset <= virtualExtmark.start) {\n this.editBuffer.setCursorByOffset(virtualExtmark.end)\n return\n }\n\n this.originalMoveCursorRight()\n }\n\n this.editorView.moveUpVisual = (): void => {\n if (this.destroyed) {\n this.originalMoveUpVisual()\n return\n }\n\n const hasSelection = this.editorView.hasSelection()\n\n if (hasSelection) {\n this.originalMoveUpVisual()\n return\n }\n\n const currentOffset = this.editorView.getVisualCursor().offset\n this.originalMoveUpVisual()\n const newOffset = this.editorView.getVisualCursor().offset\n\n const virtualExtmark = this.findVirtualExtmarkContaining(newOffset)\n if (virtualExtmark) {\n const distanceToStart = newOffset - virtualExtmark.start\n const distanceToEnd = virtualExtmark.end - newOffset\n\n if (distanceToStart < distanceToEnd) {\n this.editorView.setCursorByOffset(virtualExtmark.start - 1)\n } else {\n this.editorView.setCursorByOffset(virtualExtmark.end)\n }\n }\n }\n\n this.editorView.moveDownVisual = (): void => {\n if (this.destroyed) {\n this.originalMoveDownVisual()\n return\n }\n\n const hasSelection = this.editorView.hasSelection()\n\n if (hasSelection) {\n this.originalMoveDownVisual()\n return\n }\n\n const currentOffset = this.editorView.getVisualCursor().offset\n this.originalMoveDownVisual()\n const newOffset = this.editorView.getVisualCursor().offset\n\n const virtualExtmark = this.findVirtualExtmarkContaining(newOffset)\n if (virtualExtmark) {\n const distanceToStart = newOffset - virtualExtmark.start\n const distanceToEnd = virtualExtmark.end - newOffset\n\n if (distanceToStart < distanceToEnd) {\n const adjustedOffset = virtualExtmark.start - 1\n const targetOffset = adjustedOffset <= currentOffset ? virtualExtmark.end : adjustedOffset\n this.editorView.setCursorByOffset(targetOffset)\n } else {\n this.editorView.setCursorByOffset(virtualExtmark.end)\n }\n }\n }\n\n this.editBuffer.setCursorByOffset = (offset: number): void => {\n if (this.destroyed) {\n this.originalSetCursorByOffset(offset)\n return\n }\n\n const currentOffset = this.editorView.getVisualCursor().offset\n const hasSelection = this.editorView.hasSelection()\n\n if (hasSelection) {\n this.originalSetCursorByOffset(offset)\n return\n }\n\n const movingForward = offset > currentOffset\n\n if (movingForward) {\n const virtualExtmark = this.findVirtualExtmarkContaining(offset)\n if (virtualExtmark && currentOffset <= virtualExtmark.start) {\n this.originalSetCursorByOffset(virtualExtmark.end)\n return\n }\n } else {\n for (const extmark of this.extmarks.values()) {\n if (extmark.virtual && currentOffset >= extmark.end && offset < extmark.end && offset >= extmark.start) {\n this.originalSetCursorByOffset(extmark.start - 1)\n return\n }\n }\n }\n\n this.originalSetCursorByOffset(offset)\n }\n }\n\n private wrapDeletion(): void {\n this.editBuffer.deleteCharBackward = (): void => {\n if (this.destroyed) {\n this.originalDeleteCharBackward()\n return\n }\n\n this.saveSnapshot()\n\n const currentOffset = this.editorView.getVisualCursor().offset\n const hadSelection = this.editorView.hasSelection()\n\n if (currentOffset === 0) {\n this.originalDeleteCharBackward()\n return\n }\n\n if (hadSelection) {\n this.originalDeleteCharBackward()\n return\n }\n\n const targetOffset = currentOffset - 1\n const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n\n if (virtualExtmark && currentOffset === virtualExtmark.end) {\n const startCursor = this.offsetToPosition(virtualExtmark.start)\n const endCursor = this.offsetToPosition(virtualExtmark.end)\n const deleteOffset = virtualExtmark.start\n const deleteLength = virtualExtmark.end - virtualExtmark.start\n\n this.deleteExtmarkById(virtualExtmark.id)\n\n this.originalDeleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)\n this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n\n this.updateHighlights()\n\n return\n }\n\n this.originalDeleteCharBackward()\n this.adjustExtmarksAfterDeletion(targetOffset, 1)\n }\n\n this.editBuffer.deleteChar = (): void => {\n if (this.destroyed) {\n this.originalDeleteChar()\n return\n }\n\n this.saveSnapshot()\n\n const currentOffset = this.editorView.getVisualCursor().offset\n const textLength = this.editBuffer.getText().length\n const hadSelection = this.editorView.hasSelection()\n\n if (currentOffset >= textLength) {\n this.originalDeleteChar()\n return\n }\n\n if (hadSelection) {\n this.originalDeleteChar()\n return\n }\n\n const targetOffset = currentOffset\n const virtualExtmark = this.findVirtualExtmarkContaining(targetOffset)\n\n if (virtualExtmark && currentOffset === virtualExtmark.start) {\n const startCursor = this.offsetToPosition(virtualExtmark.start)\n const endCursor = this.offsetToPosition(virtualExtmark.end)\n const deleteOffset = virtualExtmark.start\n const deleteLength = virtualExtmark.end - virtualExtmark.start\n\n this.deleteExtmarkById(virtualExtmark.id)\n\n this.originalDeleteRange(startCursor.row, startCursor.col, endCursor.row, endCursor.col)\n this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n\n this.updateHighlights()\n\n return\n }\n\n this.originalDeleteChar()\n this.adjustExtmarksAfterDeletion(targetOffset, 1)\n }\n\n this.editBuffer.deleteRange = (startLine: number, startCol: number, endLine: number, endCol: number): void => {\n if (this.destroyed) {\n this.originalDeleteRange(startLine, startCol, endLine, endCol)\n return\n }\n\n this.saveSnapshot()\n\n const startOffset = this.positionToOffset(startLine, startCol)\n const endOffset = this.positionToOffset(endLine, endCol)\n const length = endOffset - startOffset\n\n this.originalDeleteRange(startLine, startCol, endLine, endCol)\n this.adjustExtmarksAfterDeletion(startOffset, length)\n }\n\n this.editBuffer.deleteLine = (): void => {\n if (this.destroyed) {\n this.originalDeleteLine()\n return\n }\n\n this.saveSnapshot()\n\n const text = this.editBuffer.getText()\n const currentOffset = this.editorView.getVisualCursor().offset\n\n let lineStart = 0\n for (let i = currentOffset - 1; i >= 0; i--) {\n if (text[i] === \"\\n\") {\n lineStart = i + 1\n break\n }\n }\n\n let lineEnd = text.length\n for (let i = currentOffset; i < text.length; i++) {\n if (text[i] === \"\\n\") {\n lineEnd = i + 1\n break\n }\n }\n\n const deleteLength = lineEnd - lineStart\n\n this.originalDeleteLine()\n this.adjustExtmarksAfterDeletion(lineStart, deleteLength)\n }\n }\n\n private wrapInsertion(): void {\n this.editBuffer.insertText = (text: string): void => {\n if (this.destroyed) {\n this.originalInsertText(text)\n return\n }\n\n this.saveSnapshot()\n\n const currentOffset = this.editorView.getVisualCursor().offset\n this.originalInsertText(text)\n this.adjustExtmarksAfterInsertion(currentOffset, text.length)\n }\n\n this.editBuffer.insertChar = (char: string): void => {\n if (this.destroyed) {\n this.originalInsertChar(char)\n return\n }\n\n this.saveSnapshot()\n\n const currentOffset = this.editorView.getVisualCursor().offset\n this.originalInsertChar(char)\n this.adjustExtmarksAfterInsertion(currentOffset, 1)\n }\n\n this.editBuffer.setText = (text: string): void => {\n if (this.destroyed) {\n this.originalSetText(text)\n return\n }\n\n this.clear()\n this.originalSetText(text)\n }\n\n this.editBuffer.replaceText = (text: string): void => {\n if (this.destroyed) {\n this.originalReplaceText(text)\n return\n }\n\n this.saveSnapshot()\n this.clear()\n this.originalReplaceText(text)\n }\n\n this.editBuffer.clear = (): void => {\n if (this.destroyed) {\n this.originalClear()\n return\n }\n\n this.saveSnapshot()\n\n this.clear()\n this.originalClear()\n }\n\n this.editBuffer.newLine = (): void => {\n if (this.destroyed) {\n this.originalNewLine()\n return\n }\n\n this.saveSnapshot()\n\n const currentOffset = this.editorView.getVisualCursor().offset\n this.originalNewLine()\n this.adjustExtmarksAfterInsertion(currentOffset, 1)\n }\n }\n\n private wrapEditorViewDeleteSelectedText(): void {\n this.editorView.deleteSelectedText = (): void => {\n if (this.destroyed) {\n this.originalEditorViewDeleteSelectedText()\n return\n }\n\n this.saveSnapshot()\n\n const selection = this.editorView.getSelection()\n if (!selection) {\n this.originalEditorViewDeleteSelectedText()\n return\n }\n\n const deleteOffset = Math.min(selection.start, selection.end)\n const deleteLength = Math.abs(selection.end - selection.start)\n\n this.originalEditorViewDeleteSelectedText()\n\n if (deleteLength > 0) {\n this.adjustExtmarksAfterDeletion(deleteOffset, deleteLength)\n }\n }\n }\n\n private setupContentChangeListener(): void {\n this.editBuffer.on(\"content-changed\", () => {\n if (this.destroyed) return\n this.updateHighlights()\n })\n }\n\n private deleteExtmarkById(id: number): void {\n const extmark = this.extmarks.get(id)\n if (extmark) {\n this.extmarks.delete(id)\n this.extmarksByTypeId.get(extmark.typeId)?.delete(id)\n this.metadata.delete(id)\n }\n }\n\n private findVirtualExtmarkContaining(offset: number): Extmark | null {\n for (const extmark of this.extmarks.values()) {\n if (extmark.virtual && offset >= extmark.start && offset < extmark.end) {\n return extmark\n }\n }\n return null\n }\n\n private adjustExtmarksAfterInsertion(insertOffset: number, length: number): void {\n for (const extmark of this.extmarks.values()) {\n if (extmark.start >= insertOffset) {\n extmark.start += length\n extmark.end += length\n } else if (extmark.end > insertOffset) {\n extmark.end += length\n }\n }\n this.updateHighlights()\n }\n\n public adjustExtmarksAfterDeletion(deleteOffset: number, length: number): void {\n const toDelete: number[] = []\n\n for (const extmark of this.extmarks.values()) {\n if (extmark.end <= deleteOffset) {\n continue\n }\n\n if (extmark.start >= deleteOffset + length) {\n extmark.start -= length\n extmark.end -= length\n } else if (extmark.start >= deleteOffset && extmark.end <= deleteOffset + length) {\n toDelete.push(extmark.id)\n } else if (extmark.start < deleteOffset && extmark.end > deleteOffset + length) {\n extmark.end -= length\n } else if (extmark.start < deleteOffset && extmark.end > deleteOffset) {\n extmark.end -= Math.min(extmark.end, deleteOffset + length) - deleteOffset\n } else if (extmark.start < deleteOffset + length && extmark.end > deleteOffset + length) {\n const overlap = deleteOffset + length - extmark.start\n extmark.start = deleteOffset\n extmark.end -= length\n }\n }\n\n for (const id of toDelete) {\n this.deleteExtmarkById(id)\n }\n\n this.updateHighlights()\n }\n\n private offsetToPosition(offset: number): { row: number; col: number } {\n const result = this.editBuffer.offsetToPosition(offset)\n if (!result) {\n return { row: 0, col: 0 }\n }\n return result\n }\n\n private positionToOffset(row: number, col: number): number {\n return this.editBuffer.positionToOffset(row, col)\n }\n\n private updateHighlights(): void {\n this.editBuffer.clearAllHighlights()\n\n for (const extmark of this.extmarks.values()) {\n if (extmark.styleId !== undefined) {\n // extmark.start/end are display-width offsets including newlines (from cursor operations)\n // addHighlightByCharRange expects display-width offsets excluding newlines\n // So we need to subtract the number of newlines before each position\n const startWithoutNewlines = this.offsetExcludingNewlines(extmark.start)\n const endWithoutNewlines = this.offsetExcludingNewlines(extmark.end)\n\n this.editBuffer.addHighlightByCharRange({\n start: startWithoutNewlines,\n end: endWithoutNewlines,\n styleId: extmark.styleId,\n priority: extmark.priority ?? 0,\n hlRef: extmark.id,\n })\n }\n }\n }\n\n private offsetExcludingNewlines(offset: number): number {\n // offset is a display-width offset from the start of the buffer (includes newlines)\n // We need to convert to display-width excluding newlines\n // This means: subtract 1 for each newline encountered before this offset\n const text = this.editBuffer.getText()\n let displayWidthSoFar = 0\n let newlineCount = 0\n\n // Walk through the text and calculate display widths\n let i = 0\n while (i < text.length && displayWidthSoFar < offset) {\n if (text[i] === \"\\n\") {\n displayWidthSoFar++ // newline counts as width 1 in cursor offset\n newlineCount++\n i++\n } else {\n // Find the next newline or end of string\n let j = i\n while (j < text.length && text[j] !== \"\\n\") {\n j++\n }\n const chunk = text.substring(i, j)\n const chunkWidth = Bun.stringWidth(chunk)\n\n if (displayWidthSoFar + chunkWidth < offset) {\n // Entire chunk fits before offset\n displayWidthSoFar += chunkWidth\n i = j\n } else {\n // Offset is within this chunk - need to find exact position\n // Walk character by character\n for (let k = i; k < j && displayWidthSoFar < offset; k++) {\n const charWidth = Bun.stringWidth(text[k])\n displayWidthSoFar += charWidth\n }\n break\n }\n }\n }\n\n return offset - newlineCount\n }\n\n public create(options: ExtmarkOptions): number {\n if (this.destroyed) {\n throw new Error(\"ExtmarksController is destroyed\")\n }\n\n const id = this.nextId++\n const typeId = options.typeId ?? 0\n const extmark: Extmark = {\n id,\n start: options.start,\n end: options.end,\n virtual: options.virtual ?? false,\n styleId: options.styleId,\n priority: options.priority,\n data: options.data,\n typeId,\n }\n\n this.extmarks.set(id, extmark)\n\n if (!this.extmarksByTypeId.has(typeId)) {\n this.extmarksByTypeId.set(typeId, new Set())\n }\n this.extmarksByTypeId.get(typeId)!.add(id)\n\n if (options.metadata !== undefined) {\n this.metadata.set(id, options.metadata)\n }\n\n this.updateHighlights()\n\n return id\n }\n\n public delete(id: number): boolean {\n if (this.destroyed) {\n throw new Error(\"ExtmarksController is destroyed\")\n }\n\n const extmark = this.extmarks.get(id)\n if (!extmark) return false\n\n this.deleteExtmarkById(id)\n this.updateHighlights()\n\n return true\n }\n\n public get(id: number): Extmark | null {\n if (this.destroyed) return null\n return this.extmarks.get(id) ?? null\n }\n\n public getAll(): Extmark[] {\n if (this.destroyed) return []\n return Array.from(this.extmarks.values())\n }\n\n public getVirtual(): Extmark[] {\n if (this.destroyed) return []\n return Array.from(this.extmarks.values()).filter((e) => e.virtual)\n }\n\n public getAtOffset(offset: number): Extmark[] {\n if (this.destroyed) return []\n return Array.from(this.extmarks.values()).filter((e) => offset >= e.start && offset < e.end)\n }\n\n public getAllForTypeId(typeId: number): Extmark[] {\n if (this.destroyed) return []\n const ids = this.extmarksByTypeId.get(typeId)\n if (!ids) return []\n return Array.from(ids)\n .map((id) => this.extmarks.get(id))\n .filter((e): e is Extmark => e !== undefined)\n }\n\n public clear(): void {\n if (this.destroyed) return\n\n this.extmarks.clear()\n this.extmarksByTypeId.clear()\n this.metadata.clear()\n this.updateHighlights()\n }\n\n private saveSnapshot(): void {\n this.history.saveSnapshot(this.extmarks, this.nextId)\n }\n\n private restoreSnapshot(snapshot: ExtmarksSnapshot): void {\n this.extmarks = new Map(Array.from(snapshot.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }]))\n this.nextId = snapshot.nextId\n this.updateHighlights()\n }\n\n private wrapUndoRedo(): void {\n this.editBuffer.undo = (): string | null => {\n if (this.destroyed) {\n return this.originalUndo()\n }\n\n if (!this.history.canUndo()) {\n return this.originalUndo()\n }\n\n const currentSnapshot: ExtmarksSnapshot = {\n extmarks: new Map(Array.from(this.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n nextId: this.nextId,\n }\n this.history.pushRedo(currentSnapshot)\n\n const snapshot = this.history.undo()!\n this.restoreSnapshot(snapshot)\n\n return this.originalUndo()\n }\n\n this.editBuffer.redo = (): string | null => {\n if (this.destroyed) {\n return this.originalRedo()\n }\n\n if (!this.history.canRedo()) {\n return this.originalRedo()\n }\n\n const currentSnapshot: ExtmarksSnapshot = {\n extmarks: new Map(Array.from(this.extmarks.entries()).map(([id, extmark]) => [id, { ...extmark }])),\n nextId: this.nextId,\n }\n this.history.pushUndo(currentSnapshot)\n\n const snapshot = this.history.redo()!\n this.restoreSnapshot(snapshot)\n\n return this.originalRedo()\n }\n }\n\n public registerType(typeName: string): number {\n if (this.destroyed) {\n throw new Error(\"ExtmarksController is destroyed\")\n }\n\n const existing = this.typeNameToId.get(typeName)\n if (existing !== undefined) {\n return existing\n }\n\n const typeId = this.nextTypeId++\n this.typeNameToId.set(typeName, typeId)\n this.typeIdToName.set(typeId, typeName)\n return typeId\n }\n\n public getTypeId(typeName: string): number | null {\n if (this.destroyed) return null\n return this.typeNameToId.get(typeName) ?? null\n }\n\n public getTypeName(typeId: number): string | null {\n if (this.destroyed) return null\n return this.typeIdToName.get(typeId) ?? null\n }\n\n public getMetadataFor(extmarkId: number): any {\n if (this.destroyed) return undefined\n return this.metadata.get(extmarkId)\n }\n\n public destroy(): void {\n if (this.destroyed) return\n\n this.editBuffer.moveCursorLeft = this.originalMoveCursorLeft\n this.editBuffer.moveCursorRight = this.originalMoveCursorRight\n this.editBuffer.setCursorByOffset = this.originalSetCursorByOffset\n this.editorView.moveUpVisual = this.originalMoveUpVisual\n this.editorView.moveDownVisual = this.originalMoveDownVisual\n this.editBuffer.deleteCharBackward = this.originalDeleteCharBackward\n this.editBuffer.deleteChar = this.originalDeleteChar\n this.editBuffer.insertText = this.originalInsertText\n this.editBuffer.insertChar = this.originalInsertChar\n this.editBuffer.deleteRange = this.originalDeleteRange\n this.editBuffer.setText = this.originalSetText\n this.editBuffer.replaceText = this.originalReplaceText\n this.editBuffer.clear = this.originalClear\n this.editBuffer.newLine = this.originalNewLine\n this.editBuffer.deleteLine = this.originalDeleteLine\n this.editorView.deleteSelectedText = this.originalEditorViewDeleteSelectedText\n this.editBuffer.undo = this.originalUndo\n this.editBuffer.redo = this.originalRedo\n\n this.extmarks.clear()\n this.extmarksByTypeId.clear()\n this.metadata.clear()\n this.typeNameToId.clear()\n this.typeIdToName.clear()\n this.history.clear()\n this.destroyed = true\n }\n}\n\nexport function createExtmarksController(editBuffer: EditBuffer, editorView: EditorView): ExtmarksController {\n return new ExtmarksController(editBuffer, editorView)\n}\n",
41
- "import { SystemClock, type Clock, type TimerHandle } from \"./clock\"\n\ntype Hex = string | null\n\nconst SYSTEM_CLOCK = new SystemClock()\n\nconst OSC4_RESPONSE =\n /\\x1b]4;(\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nconst OSC_SPECIAL_RESPONSE =\n /\\x1b](\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nexport type WriteFunction = (data: string | Buffer) => boolean\n\nexport interface TerminalColors {\n palette: Hex[]\n defaultForeground: Hex\n defaultBackground: Hex\n cursorColor: Hex\n mouseForeground: Hex\n mouseBackground: Hex\n tekForeground: Hex\n tekBackground: Hex\n highlightBackground: Hex\n highlightForeground: Hex\n}\n\nexport interface GetPaletteOptions {\n timeout?: number\n size?: number\n}\n\nexport interface TerminalPaletteDetector {\n detect(options?: GetPaletteOptions): Promise<TerminalColors>\n detectOSCSupport(timeoutMs?: number): Promise<boolean>\n cleanup(): void\n}\n\nexport type OscSubscriptionSource = {\n subscribeOsc(handler: (sequence: string) => void): () => void\n}\n\nfunction scaleComponent(comp: string): string {\n const val = parseInt(comp, 16)\n const maxIn = (1 << (4 * comp.length)) - 1\n return Math.round((val / maxIn) * 255)\n .toString(16)\n .padStart(2, \"0\")\n}\n\nfunction toHex(r?: string, g?: string, b?: string, hex6?: string): string {\n if (hex6) return `#${hex6.toLowerCase()}`\n if (r && g && b) return `#${scaleComponent(r)}${scaleComponent(g)}${scaleComponent(b)}`\n return \"#000000\"\n}\n\n/**\n * Wrap OSC sequence for tmux passthrough\n * tmux requires DCS sequences to pass OSC to the underlying terminal\n * Format: ESC P tmux; ESC <OSC_SEQUENCE> ESC \\\n */\nfunction wrapForTmux(osc: string): string {\n // Replace ESC with ESC ESC for tmux (escape the escape)\n const escaped = osc.replace(/\\x1b/g, \"\\x1b\\x1b\")\n return `\\x1bPtmux;${escaped}\\x1b\\\\`\n}\n\nexport class TerminalPalette implements TerminalPaletteDetector {\n private stdin: NodeJS.ReadStream\n private stdout: NodeJS.WriteStream\n private writeFn: WriteFunction\n private activeQuerySessions: Array<() => void> = []\n private inLegacyTmux: boolean\n private oscSource?: OscSubscriptionSource\n private readonly clock: Clock\n\n constructor(\n stdin: NodeJS.ReadStream,\n stdout: NodeJS.WriteStream,\n writeFn?: WriteFunction,\n isLegacyTmux?: boolean,\n oscSource?: OscSubscriptionSource,\n clock?: Clock,\n ) {\n this.stdin = stdin\n this.stdout = stdout\n this.writeFn = writeFn || ((data: string | Buffer) => stdout.write(data))\n this.inLegacyTmux = isLegacyTmux ?? false\n this.oscSource = oscSource\n this.clock = clock ?? SYSTEM_CLOCK\n }\n\n /**\n * Write an OSC sequence, wrapping for tmux if needed\n */\n private writeOsc(osc: string): boolean {\n const data = this.inLegacyTmux ? wrapForTmux(osc) : osc\n return this.writeFn(data)\n }\n\n cleanup(): void {\n for (const cleanupSession of [...this.activeQuerySessions]) {\n cleanupSession()\n }\n this.activeQuerySessions = []\n }\n\n private subscribeInput(handler: (chunk: string | Buffer) => void): () => void {\n if (this.oscSource) {\n return this.oscSource.subscribeOsc((sequence) => {\n handler(sequence)\n })\n }\n\n this.stdin.on(\"data\", handler)\n return () => {\n this.stdin.removeListener(\"data\", handler)\n }\n }\n\n private createQuerySession() {\n const timers = new Set<TimerHandle>()\n const subscriptions = new Set<() => void>()\n let closed = false\n\n const cleanup = () => {\n if (closed) return\n closed = true\n\n for (const timer of timers) {\n this.clock.clearTimeout(timer)\n }\n timers.clear()\n\n for (const unsubscribe of subscriptions) {\n unsubscribe()\n }\n subscriptions.clear()\n\n const idx = this.activeQuerySessions.indexOf(cleanup)\n if (idx !== -1) this.activeQuerySessions.splice(idx, 1)\n }\n\n this.activeQuerySessions.push(cleanup)\n\n return {\n setTimer: (fn: () => void, ms: number): TimerHandle => {\n const timer = this.clock.setTimeout(fn, ms)\n timers.add(timer)\n return timer\n },\n resetTimer: (existing: TimerHandle | null, fn: () => void, ms: number): TimerHandle => {\n if (existing) {\n this.clock.clearTimeout(existing)\n timers.delete(existing)\n }\n\n const timer = this.clock.setTimeout(fn, ms)\n timers.add(timer)\n return timer\n },\n subscribeInput: (handler: (chunk: string | Buffer) => void): (() => void) => {\n const unsubscribe = this.subscribeInput(handler)\n subscriptions.add(unsubscribe)\n return () => {\n if (!subscriptions.has(unsubscribe)) return\n subscriptions.delete(unsubscribe)\n unsubscribe()\n }\n },\n cleanup,\n }\n }\n\n async detectOSCSupport(timeoutMs = 300): Promise<boolean> {\n const out = this.stdout\n\n if (!out.isTTY || !this.stdin.isTTY) return false\n\n return new Promise<boolean>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let settled = false\n\n const finish = (supported: boolean) => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(supported)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n // Reset regex lastIndex before testing due to global flag\n OSC4_RESPONSE.lastIndex = 0\n if (OSC4_RESPONSE.test(buffer)) {\n finish(true)\n }\n }\n\n session.setTimer(() => {\n finish(false)\n }, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(\"\\x1b]4;0;?\\x07\")\n })\n }\n\n private async queryPalette(indices: number[], timeoutMs = 1200): Promise<Map<number, Hex>> {\n const out = this.stdout\n const results = new Map<number, Hex>()\n indices.forEach((i) => results.set(i, null))\n\n if (!out.isTTY || !this.stdin.isTTY) {\n return results\n }\n\n return new Promise<Map<number, Hex>>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let idleTimer: TimerHandle | null = null\n let settled = false\n\n const finish = () => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(results)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n\n let m: RegExpExecArray | null\n OSC4_RESPONSE.lastIndex = 0\n while ((m = OSC4_RESPONSE.exec(buffer))) {\n const idx = parseInt(m[1], 10)\n if (results.has(idx)) results.set(idx, toHex(m[2], m[3], m[4], m[5]))\n }\n\n if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n const done = [...results.values()].filter((v) => v !== null).length\n if (done === results.size) {\n finish()\n return\n }\n\n idleTimer = session.resetTimer(idleTimer, finish, 150)\n }\n\n session.setTimer(finish, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(indices.map((i) => `\\x1b]4;${i};?\\x07`).join(\"\"))\n })\n }\n\n private async querySpecialColors(timeoutMs = 1200): Promise<Record<number, Hex>> {\n const out = this.stdout\n const results: Record<number, Hex> = {\n 10: null,\n 11: null,\n 12: null,\n 13: null,\n 14: null,\n 15: null,\n 16: null,\n 17: null,\n 19: null,\n }\n\n if (!out.isTTY || !this.stdin.isTTY) {\n return results\n }\n\n return new Promise<Record<number, Hex>>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let idleTimer: TimerHandle | null = null\n let settled = false\n\n const finish = () => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(results)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n let updated = false\n\n let m: RegExpExecArray | null\n OSC_SPECIAL_RESPONSE.lastIndex = 0\n while ((m = OSC_SPECIAL_RESPONSE.exec(buffer))) {\n const idx = parseInt(m[1], 10)\n if (idx in results) {\n results[idx] = toHex(m[2], m[3], m[4], m[5])\n updated = true\n }\n }\n\n if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n const done = Object.values(results).filter((v) => v !== null).length\n if (done === Object.keys(results).length) {\n finish()\n return\n }\n\n if (!updated) return\n\n idleTimer = session.resetTimer(idleTimer, finish, 150)\n }\n\n session.setTimer(finish, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(\n [\n \"\\x1b]10;?\\x07\",\n \"\\x1b]11;?\\x07\",\n \"\\x1b]12;?\\x07\",\n \"\\x1b]13;?\\x07\",\n \"\\x1b]14;?\\x07\",\n \"\\x1b]15;?\\x07\",\n \"\\x1b]16;?\\x07\",\n \"\\x1b]17;?\\x07\",\n \"\\x1b]19;?\\x07\",\n ].join(\"\"),\n )\n })\n }\n\n async detect(options?: GetPaletteOptions): Promise<TerminalColors> {\n const { timeout = 5000, size = 16 } = options || {}\n const supported = await this.detectOSCSupport()\n\n if (!supported) {\n return {\n palette: Array(size).fill(null),\n defaultForeground: null,\n defaultBackground: null,\n cursorColor: null,\n mouseForeground: null,\n mouseBackground: null,\n tekForeground: null,\n tekBackground: null,\n highlightBackground: null,\n highlightForeground: null,\n }\n }\n\n const indicesToQuery = [...Array(size).keys()]\n const [paletteResults, specialColors] = await Promise.all([\n this.queryPalette(indicesToQuery, timeout),\n this.querySpecialColors(timeout),\n ])\n\n return {\n palette: [...Array(size).keys()].map((i) => paletteResults.get(i) ?? null),\n defaultForeground: specialColors[10],\n defaultBackground: specialColors[11],\n cursorColor: specialColors[12],\n mouseForeground: specialColors[13],\n mouseBackground: specialColors[14],\n tekForeground: specialColors[15],\n tekBackground: specialColors[16],\n highlightBackground: specialColors[17],\n highlightForeground: specialColors[19],\n }\n }\n}\n\nexport function createTerminalPalette(\n stdin: NodeJS.ReadStream,\n stdout: NodeJS.WriteStream,\n writeFn?: WriteFunction,\n isLegacyTmux?: boolean,\n oscSource?: OscSubscriptionSource,\n clock?: Clock,\n): TerminalPaletteDetector {\n return new TerminalPalette(stdin, stdout, writeFn, isLegacyTmux, oscSource, clock)\n}\n",
41
+ "import { SystemClock, type Clock, type TimerHandle } from \"./clock.js\"\n\ntype Hex = string | null\n\nconst SYSTEM_CLOCK = new SystemClock()\n\nconst OSC4_RESPONSE =\n /\\x1b]4;(\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nconst OSC_SPECIAL_RESPONSE =\n /\\x1b](\\d+);(?:(?:rgb:)([0-9a-fA-F]+)\\/([0-9a-fA-F]+)\\/([0-9a-fA-F]+)|#([0-9a-fA-F]{6}))(?:\\x07|\\x1b\\\\)/g\n\nexport type WriteFunction = (data: string | Buffer) => boolean\n\nexport interface TerminalColors {\n palette: Hex[]\n defaultForeground: Hex\n defaultBackground: Hex\n cursorColor: Hex\n mouseForeground: Hex\n mouseBackground: Hex\n tekForeground: Hex\n tekBackground: Hex\n highlightBackground: Hex\n highlightForeground: Hex\n}\n\nexport interface GetPaletteOptions {\n timeout?: number\n size?: number\n}\n\nexport interface TerminalPaletteDetector {\n detect(options?: GetPaletteOptions): Promise<TerminalColors>\n detectOSCSupport(timeoutMs?: number): Promise<boolean>\n cleanup(): void\n}\n\nexport type OscSubscriptionSource = {\n subscribeOsc(handler: (sequence: string) => void): () => void\n}\n\nfunction scaleComponent(comp: string): string {\n const val = parseInt(comp, 16)\n const maxIn = (1 << (4 * comp.length)) - 1\n return Math.round((val / maxIn) * 255)\n .toString(16)\n .padStart(2, \"0\")\n}\n\nfunction toHex(r?: string, g?: string, b?: string, hex6?: string): string {\n if (hex6) return `#${hex6.toLowerCase()}`\n if (r && g && b) return `#${scaleComponent(r)}${scaleComponent(g)}${scaleComponent(b)}`\n return \"#000000\"\n}\n\n/**\n * Wrap OSC sequence for tmux passthrough\n * tmux requires DCS sequences to pass OSC to the underlying terminal\n * Format: ESC P tmux; ESC <OSC_SEQUENCE> ESC \\\n */\nfunction wrapForTmux(osc: string): string {\n // Replace ESC with ESC ESC for tmux (escape the escape)\n const escaped = osc.replace(/\\x1b/g, \"\\x1b\\x1b\")\n return `\\x1bPtmux;${escaped}\\x1b\\\\`\n}\n\nexport class TerminalPalette implements TerminalPaletteDetector {\n private stdin: NodeJS.ReadStream\n private stdout: NodeJS.WriteStream\n private writeFn: WriteFunction\n private activeQuerySessions: Array<() => void> = []\n private inLegacyTmux: boolean\n private oscSource?: OscSubscriptionSource\n private readonly clock: Clock\n\n constructor(\n stdin: NodeJS.ReadStream,\n stdout: NodeJS.WriteStream,\n writeFn?: WriteFunction,\n isLegacyTmux?: boolean,\n oscSource?: OscSubscriptionSource,\n clock?: Clock,\n ) {\n this.stdin = stdin\n this.stdout = stdout\n this.writeFn = writeFn || ((data: string | Buffer) => stdout.write(data))\n this.inLegacyTmux = isLegacyTmux ?? false\n this.oscSource = oscSource\n this.clock = clock ?? SYSTEM_CLOCK\n }\n\n /**\n * Write an OSC sequence, wrapping for tmux if needed\n */\n private writeOsc(osc: string): boolean {\n const data = this.inLegacyTmux ? wrapForTmux(osc) : osc\n return this.writeFn(data)\n }\n\n cleanup(): void {\n for (const cleanupSession of [...this.activeQuerySessions]) {\n cleanupSession()\n }\n this.activeQuerySessions = []\n }\n\n private subscribeInput(handler: (chunk: string | Buffer) => void): () => void {\n if (this.oscSource) {\n return this.oscSource.subscribeOsc((sequence) => {\n handler(sequence)\n })\n }\n\n this.stdin.on(\"data\", handler)\n return () => {\n this.stdin.removeListener(\"data\", handler)\n }\n }\n\n private createQuerySession() {\n const timers = new Set<TimerHandle>()\n const subscriptions = new Set<() => void>()\n let closed = false\n\n const cleanup = () => {\n if (closed) return\n closed = true\n\n for (const timer of timers) {\n this.clock.clearTimeout(timer)\n }\n timers.clear()\n\n for (const unsubscribe of subscriptions) {\n unsubscribe()\n }\n subscriptions.clear()\n\n const idx = this.activeQuerySessions.indexOf(cleanup)\n if (idx !== -1) this.activeQuerySessions.splice(idx, 1)\n }\n\n this.activeQuerySessions.push(cleanup)\n\n return {\n setTimer: (fn: () => void, ms: number): TimerHandle => {\n const timer = this.clock.setTimeout(fn, ms)\n timers.add(timer)\n return timer\n },\n resetTimer: (existing: TimerHandle | null, fn: () => void, ms: number): TimerHandle => {\n if (existing) {\n this.clock.clearTimeout(existing)\n timers.delete(existing)\n }\n\n const timer = this.clock.setTimeout(fn, ms)\n timers.add(timer)\n return timer\n },\n subscribeInput: (handler: (chunk: string | Buffer) => void): (() => void) => {\n const unsubscribe = this.subscribeInput(handler)\n subscriptions.add(unsubscribe)\n return () => {\n if (!subscriptions.has(unsubscribe)) return\n subscriptions.delete(unsubscribe)\n unsubscribe()\n }\n },\n cleanup,\n }\n }\n\n async detectOSCSupport(timeoutMs = 300): Promise<boolean> {\n const out = this.stdout\n\n if (!out.isTTY || !this.stdin.isTTY) return false\n\n return new Promise<boolean>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let settled = false\n\n const finish = (supported: boolean) => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(supported)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n // Reset regex lastIndex before testing due to global flag\n OSC4_RESPONSE.lastIndex = 0\n if (OSC4_RESPONSE.test(buffer)) {\n finish(true)\n }\n }\n\n session.setTimer(() => {\n finish(false)\n }, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(\"\\x1b]4;0;?\\x07\")\n })\n }\n\n private async queryPalette(indices: number[], timeoutMs = 1200): Promise<Map<number, Hex>> {\n const out = this.stdout\n const results = new Map<number, Hex>()\n indices.forEach((i) => results.set(i, null))\n\n if (!out.isTTY || !this.stdin.isTTY) {\n return results\n }\n\n return new Promise<Map<number, Hex>>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let idleTimer: TimerHandle | null = null\n let settled = false\n\n const finish = () => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(results)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n\n let m: RegExpExecArray | null\n OSC4_RESPONSE.lastIndex = 0\n while ((m = OSC4_RESPONSE.exec(buffer))) {\n const idx = parseInt(m[1], 10)\n if (results.has(idx)) results.set(idx, toHex(m[2], m[3], m[4], m[5]))\n }\n\n if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n const done = [...results.values()].filter((v) => v !== null).length\n if (done === results.size) {\n finish()\n return\n }\n\n idleTimer = session.resetTimer(idleTimer, finish, 150)\n }\n\n session.setTimer(finish, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(indices.map((i) => `\\x1b]4;${i};?\\x07`).join(\"\"))\n })\n }\n\n private async querySpecialColors(timeoutMs = 1200): Promise<Record<number, Hex>> {\n const out = this.stdout\n const results: Record<number, Hex> = {\n 10: null,\n 11: null,\n 12: null,\n 13: null,\n 14: null,\n 15: null,\n 16: null,\n 17: null,\n 19: null,\n }\n\n if (!out.isTTY || !this.stdin.isTTY) {\n return results\n }\n\n return new Promise<Record<number, Hex>>((resolve) => {\n const session = this.createQuerySession()\n let buffer = \"\"\n let idleTimer: TimerHandle | null = null\n let settled = false\n\n const finish = () => {\n if (settled) return\n settled = true\n session.cleanup()\n resolve(results)\n }\n\n const onData = (chunk: string | Buffer) => {\n buffer += chunk.toString()\n let updated = false\n\n let m: RegExpExecArray | null\n OSC_SPECIAL_RESPONSE.lastIndex = 0\n while ((m = OSC_SPECIAL_RESPONSE.exec(buffer))) {\n const idx = parseInt(m[1], 10)\n if (idx in results) {\n results[idx] = toHex(m[2], m[3], m[4], m[5])\n updated = true\n }\n }\n\n if (buffer.length > 8192) buffer = buffer.slice(-4096)\n\n const done = Object.values(results).filter((v) => v !== null).length\n if (done === Object.keys(results).length) {\n finish()\n return\n }\n\n if (!updated) return\n\n idleTimer = session.resetTimer(idleTimer, finish, 150)\n }\n\n session.setTimer(finish, timeoutMs)\n session.subscribeInput(onData)\n this.writeOsc(\n [\n \"\\x1b]10;?\\x07\",\n \"\\x1b]11;?\\x07\",\n \"\\x1b]12;?\\x07\",\n \"\\x1b]13;?\\x07\",\n \"\\x1b]14;?\\x07\",\n \"\\x1b]15;?\\x07\",\n \"\\x1b]16;?\\x07\",\n \"\\x1b]17;?\\x07\",\n \"\\x1b]19;?\\x07\",\n ].join(\"\"),\n )\n })\n }\n\n async detect(options?: GetPaletteOptions): Promise<TerminalColors> {\n const { timeout = 5000, size = 16 } = options || {}\n const supported = await this.detectOSCSupport()\n\n if (!supported) {\n return {\n palette: Array(size).fill(null),\n defaultForeground: null,\n defaultBackground: null,\n cursorColor: null,\n mouseForeground: null,\n mouseBackground: null,\n tekForeground: null,\n tekBackground: null,\n highlightBackground: null,\n highlightForeground: null,\n }\n }\n\n const indicesToQuery = [...Array(size).keys()]\n const [paletteResults, specialColors] = await Promise.all([\n this.queryPalette(indicesToQuery, timeout),\n this.querySpecialColors(timeout),\n ])\n\n return {\n palette: [...Array(size).keys()].map((i) => paletteResults.get(i) ?? null),\n defaultForeground: specialColors[10],\n defaultBackground: specialColors[11],\n cursorColor: specialColors[12],\n mouseForeground: specialColors[13],\n mouseBackground: specialColors[14],\n tekForeground: specialColors[15],\n tekBackground: specialColors[16],\n highlightBackground: specialColors[17],\n highlightForeground: specialColors[19],\n }\n }\n}\n\nexport function createTerminalPalette(\n stdin: NodeJS.ReadStream,\n stdout: NodeJS.WriteStream,\n writeFn?: WriteFunction,\n isLegacyTmux?: boolean,\n oscSource?: OscSubscriptionSource,\n clock?: Clock,\n): TerminalPaletteDetector {\n return new TerminalPalette(stdin, stdout, writeFn, isLegacyTmux, oscSource, clock)\n}\n",
42
42
  "export type PasteKind = \"text\" | \"binary\" | \"unknown\"\n\nexport interface PasteMetadata {\n mimeType?: string\n kind?: PasteKind\n}\n\nconst PASTE_TEXT_DECODER = new TextDecoder()\n\nexport function decodePasteBytes(bytes: Uint8Array): string {\n return PASTE_TEXT_DECODER.decode(bytes)\n}\n\nexport function stripAnsiSequences(text: string): string {\n return Bun.stripANSI(text)\n}\n",
43
- "import type { TextChunk } from \"../text-buffer\"\nimport type { SimpleHighlight } from \"./tree-sitter/types\"\n\nconst URL_SCOPES = [\"markup.link.url\", \"string.special.url\"]\n\nexport function detectLinks(\n chunks: TextChunk[],\n context: { content: string; highlights: SimpleHighlight[] },\n): TextChunk[] {\n const content = context.content\n const highlights = context.highlights\n\n const ranges: Array<{ start: number; end: number; url: string }> = []\n\n for (let i = 0; i < highlights.length; i++) {\n const [start, end, group] = highlights[i]\n if (!URL_SCOPES.includes(group)) continue\n\n const url = content.slice(start, end)\n ranges.push({ start, end, url })\n\n for (let j = i - 1; j >= 0; j--) {\n const [labelStart, labelEnd, prev] = highlights[j]\n if (prev === \"markup.link.label\") {\n ranges.push({ start: labelStart, end: labelEnd, url })\n break\n }\n if (!prev.startsWith(\"markup.link\")) break\n }\n }\n\n if (ranges.length === 0) return chunks\n\n // Use content.indexOf to find each chunk's position in the original content.\n // This handles concealed text correctly because concealed chunks are either\n // empty (length 0, skipped) or single-char replacements (length 1, skipped).\n // Non-concealed chunks with length > 1 are exact substrings of content in order.\n let contentPos = 0\n for (const chunk of chunks) {\n if (chunk.text.length <= 1) continue\n\n const idx = content.indexOf(chunk.text, contentPos)\n if (idx < 0) continue\n\n for (const range of ranges) {\n if (idx < range.end && idx + chunk.text.length > range.start) {\n chunk.link = { url: range.url }\n break\n }\n }\n\n contentPos = idx + chunk.text.length\n }\n\n return chunks\n}\n",
43
+ "import type { TextChunk } from \"../text-buffer.js\"\nimport type { SimpleHighlight } from \"./tree-sitter/types.js\"\n\nconst URL_SCOPES = [\"markup.link.url\", \"string.special.url\"]\n\nexport function detectLinks(\n chunks: TextChunk[],\n context: { content: string; highlights: SimpleHighlight[] },\n): TextChunk[] {\n const content = context.content\n const highlights = context.highlights\n\n const ranges: Array<{ start: number; end: number; url: string }> = []\n\n for (let i = 0; i < highlights.length; i++) {\n const [start, end, group] = highlights[i]\n if (!URL_SCOPES.includes(group)) continue\n\n const url = content.slice(start, end)\n ranges.push({ start, end, url })\n\n for (let j = i - 1; j >= 0; j--) {\n const [labelStart, labelEnd, prev] = highlights[j]\n if (prev === \"markup.link.label\") {\n ranges.push({ start: labelStart, end: labelEnd, url })\n break\n }\n if (!prev.startsWith(\"markup.link\")) break\n }\n }\n\n if (ranges.length === 0) return chunks\n\n // Use content.indexOf to find each chunk's position in the original content.\n // This handles concealed text correctly because concealed chunks are either\n // empty (length 0, skipped) or single-char replacements (length 1, skipped).\n // Non-concealed chunks with length > 1 are exact substrings of content in order.\n let contentPos = 0\n for (const chunk of chunks) {\n if (chunk.text.length <= 1) continue\n\n const idx = content.indexOf(chunk.text, contentPos)\n if (idx < 0) continue\n\n for (const range of ranges) {\n if (idx < range.end && idx + chunk.text.length > range.start) {\n chunk.link = { url: range.url }\n break\n }\n }\n\n contentPos = idx + chunk.text.length\n }\n\n return chunks\n}\n",
44
44
  "import { dlopen, toArrayBuffer, JSCallback, ptr, type Pointer } from \"bun:ffi\"\nimport { existsSync, writeFileSync } from \"fs\"\nimport { EventEmitter } from \"events\"\nimport {\n type CursorStyle,\n type CursorStyleOptions,\n type TargetChannel,\n type DebugOverlayCorner,\n type WidthMethod,\n type Highlight,\n type LineInfo,\n type MousePointerStyle,\n} from \"./types.js\"\nexport type { LineInfo, AllocatorStats, BuildOptions }\n\nimport { RGBA } from \"./lib/RGBA.js\"\nimport { OptimizedBuffer } from \"./buffer.js\"\nimport { TextBuffer } from \"./text-buffer.js\"\nimport { env, registerEnvVar } from \"./lib/env.js\"\nimport {\n StyledChunkStruct,\n HighlightStruct,\n LogicalCursorStruct,\n VisualCursorStruct,\n TerminalCapabilitiesStruct,\n EncodedCharStruct,\n LineInfoStruct,\n MeasureResultStruct,\n CursorStateStruct,\n CursorStyleOptionsStruct,\n GridDrawOptionsStruct,\n NativeSpanFeedOptionsStruct,\n NativeSpanFeedStatsStruct,\n ReserveInfoStruct,\n BuildOptionsStruct,\n AllocatorStatsStruct,\n} from \"./zig-structs.js\"\nimport type {\n NativeSpanFeedOptions,\n NativeSpanFeedStats,\n ReserveInfo,\n BuildOptions,\n AllocatorStats,\n} from \"./zig-structs.js\"\nimport { isBunfsPath } from \"./lib/bunfs.js\"\n\nconst module = await import(`@fairyhunter13/opentui-core-${process.platform}-${process.arch}/index.ts`)\nlet targetLibPath = module.default\n\nif (isBunfsPath(targetLibPath)) {\n targetLibPath = targetLibPath.replace(\"../\", \"\")\n}\n\nif (!existsSync(targetLibPath)) {\n throw new Error(`opentui is not supported on the current platform: ${process.platform}-${process.arch}`)\n}\n\nregisterEnvVar({\n name: \"OTUI_DEBUG_FFI\",\n description: \"Enable debug logging for the FFI bindings.\",\n type: \"boolean\",\n default: false,\n})\n\nregisterEnvVar({\n name: \"OTUI_TRACE_FFI\",\n description: \"Enable tracing for the FFI bindings.\",\n type: \"boolean\",\n default: false,\n})\n\n// Env vars used in terminal.zig\nregisterEnvVar({\n name: \"OPENTUI_FORCE_WCWIDTH\",\n description: \"Use wcwidth for character width calculations\",\n type: \"boolean\",\n default: false,\n})\nregisterEnvVar({\n name: \"OPENTUI_FORCE_UNICODE\",\n description: \"Force Mode 2026 Unicode support in terminal capabilities\",\n type: \"boolean\",\n default: false,\n})\nregisterEnvVar({\n name: \"OPENTUI_GRAPHICS\",\n description: \"Enable Kitty graphics protocol detection\",\n type: \"boolean\",\n default: true,\n})\nregisterEnvVar({\n name: \"OPENTUI_FORCE_NOZWJ\",\n description: \"Use no_zwj width method (Unicode without ZWJ joining)\",\n type: \"boolean\",\n default: false,\n})\n\n// Cursor & mouse pointer style mappings (avoid recreation on each call)\nconst CURSOR_STYLE_TO_ID = { block: 0, line: 1, underline: 2, default: 3 } as const\nconst CURSOR_ID_TO_STYLE = [\"block\", \"line\", \"underline\", \"default\"] as const\nconst MOUSE_STYLE_TO_ID = { default: 0, pointer: 1, text: 2, crosshair: 3, move: 4, \"not-allowed\": 5 } as const\n\n// Global singleton state for FFI tracing to prevent duplicate exit handlers\nlet globalTraceSymbols: Record<string, number[]> | null = null\nlet globalFFILogPath: string | null = null\nlet exitHandlerRegistered = false\n\nfunction toPointer(value: number | bigint): Pointer {\n if (typeof value === \"bigint\") {\n if (value > BigInt(Number.MAX_SAFE_INTEGER)) {\n throw new Error(\"Pointer exceeds safe integer range\")\n }\n return Number(value) as Pointer\n }\n return value as Pointer\n}\n\nfunction toNumber(value: number | bigint): number {\n return typeof value === \"bigint\" ? Number(value) : value\n}\n\nfunction getOpenTUILib(libPath?: string) {\n const resolvedLibPath = libPath || targetLibPath\n\n const rawSymbols = dlopen(resolvedLibPath, {\n // Logging\n setLogCallback: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n // Event bus\n setEventCallback: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n // Renderer management\n createRenderer: {\n args: [\"u32\", \"u32\", \"bool\", \"bool\"],\n returns: \"ptr\",\n },\n setTerminalEnvVar: {\n args: [\"ptr\", \"ptr\", \"usize\", \"ptr\", \"usize\"],\n returns: \"bool\",\n },\n destroyRenderer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n setUseThread: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n setBackgroundColor: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n setRenderOffset: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n updateStats: {\n args: [\"ptr\", \"f64\", \"u32\", \"f64\"],\n returns: \"void\",\n },\n updateMemoryStats: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n render: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n getNextBuffer: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n getCurrentBuffer: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n\n queryPixelResolution: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n\n createOptimizedBuffer: {\n args: [\"u32\", \"u32\", \"bool\", \"u8\", \"ptr\", \"usize\"],\n returns: \"ptr\",\n },\n destroyOptimizedBuffer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n\n drawFrameBuffer: {\n args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n getBufferWidth: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n getBufferHeight: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n bufferClear: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n bufferGetCharPtr: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n bufferGetFgPtr: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n bufferGetBgPtr: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n bufferGetAttributesPtr: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n bufferGetRespectAlpha: {\n args: [\"ptr\"],\n returns: \"bool\",\n },\n bufferSetRespectAlpha: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n bufferGetId: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n bufferGetRealCharSize: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n bufferWriteResolvedChars: {\n args: [\"ptr\", \"ptr\", \"usize\", \"bool\"],\n returns: \"u32\",\n },\n\n bufferDrawText: {\n args: [\"ptr\", \"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n returns: \"void\",\n },\n bufferSetCellWithAlphaBlending: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n returns: \"void\",\n },\n bufferSetCell: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n returns: \"void\",\n },\n bufferFillRect: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\"],\n returns: \"void\",\n },\n bufferColorMatrix: {\n args: [\"ptr\", \"ptr\", \"ptr\", \"usize\", \"f32\", \"u8\"],\n returns: \"void\",\n },\n bufferColorMatrixUniform: {\n args: [\"ptr\", \"ptr\", \"f32\", \"u8\"],\n returns: \"void\",\n },\n bufferResize: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n\n // Link API\n linkAlloc: {\n args: [\"ptr\", \"u32\"],\n returns: \"u32\",\n },\n linkGetUrl: {\n args: [\"u32\", \"ptr\", \"u32\"],\n returns: \"u32\",\n },\n attributesWithLink: {\n args: [\"u32\", \"u32\"],\n returns: \"u32\",\n },\n attributesGetLinkId: {\n args: [\"u32\"],\n returns: \"u32\",\n },\n\n resizeRenderer: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n\n // Cursor functions (now renderer-scoped)\n setCursorPosition: {\n args: [\"ptr\", \"i32\", \"i32\", \"bool\"],\n returns: \"void\",\n },\n setCursorColor: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n getCursorState: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n\n // Cursor and mouse pointer style (combined)\n setCursorStyleOptions: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n\n // Debug overlay\n setDebugOverlay: {\n args: [\"ptr\", \"bool\", \"u8\"],\n returns: \"void\",\n },\n\n // Terminal control\n clearTerminal: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n setTerminalTitle: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n copyToClipboardOSC52: {\n args: [\"ptr\", \"u8\", \"ptr\", \"usize\"],\n returns: \"bool\",\n },\n clearClipboardOSC52: {\n args: [\"ptr\", \"u8\"],\n returns: \"bool\",\n },\n\n bufferDrawSuperSampleBuffer: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\", \"u8\", \"u32\"],\n returns: \"void\",\n },\n bufferDrawPackedBuffer: {\n args: [\"ptr\", \"ptr\", \"usize\", \"u32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n bufferDrawGrayscaleBuffer: {\n args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n bufferDrawGrayscaleBufferSupersampled: {\n args: [\"ptr\", \"i32\", \"i32\", \"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n bufferDrawGrid: {\n args: [\"ptr\", \"ptr\", \"ptr\", \"ptr\", \"ptr\", \"u32\", \"ptr\", \"u32\", \"ptr\"],\n returns: \"void\",\n },\n bufferDrawBox: {\n args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"ptr\", \"u32\", \"ptr\", \"ptr\", \"ptr\", \"u32\"],\n returns: \"void\",\n },\n bufferPushScissorRect: {\n args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n bufferPopScissorRect: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n bufferClearScissorRects: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n bufferPushOpacity: {\n args: [\"ptr\", \"f32\"],\n returns: \"void\",\n },\n bufferPopOpacity: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n bufferGetCurrentOpacity: {\n args: [\"ptr\"],\n returns: \"f32\",\n },\n bufferClearOpacity: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n\n addToHitGrid: {\n args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n clearCurrentHitGrid: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n hitGridPushScissorRect: {\n args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n hitGridPopScissorRect: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n hitGridClearScissorRects: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n addToCurrentHitGridClipped: {\n args: [\"ptr\", \"i32\", \"i32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n checkHit: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"u32\",\n },\n getHitGridDirty: {\n args: [\"ptr\"],\n returns: \"bool\",\n },\n dumpHitGrid: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n dumpBuffers: {\n args: [\"ptr\", \"i64\"],\n returns: \"void\",\n },\n dumpStdoutBuffer: {\n args: [\"ptr\", \"i64\"],\n returns: \"void\",\n },\n restoreTerminalModes: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n enableMouse: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n disableMouse: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n enableKittyKeyboard: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n disableKittyKeyboard: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n setKittyKeyboardFlags: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n getKittyKeyboardFlags: {\n args: [\"ptr\"],\n returns: \"u8\",\n },\n setupTerminal: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n suspendRenderer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n resumeRenderer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n writeOut: {\n args: [\"ptr\", \"ptr\", \"u64\"],\n returns: \"void\",\n },\n\n // TextBuffer functions\n createTextBuffer: {\n args: [\"u8\"],\n returns: \"ptr\",\n },\n destroyTextBuffer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferGetLength: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n textBufferGetByteSize: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n\n textBufferReset: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferClear: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferSetDefaultFg: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferSetDefaultBg: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferSetDefaultAttributes: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferResetDefaults: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferGetTabWidth: {\n args: [\"ptr\"],\n returns: \"u8\",\n },\n textBufferSetTabWidth: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n textBufferRegisterMemBuffer: {\n args: [\"ptr\", \"ptr\", \"usize\", \"bool\"],\n returns: \"u16\",\n },\n textBufferReplaceMemBuffer: {\n args: [\"ptr\", \"u8\", \"ptr\", \"usize\", \"bool\"],\n returns: \"bool\",\n },\n textBufferClearMemRegistry: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferSetTextFromMem: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n textBufferAppend: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n textBufferAppendFromMemId: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n textBufferLoadFile: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"bool\",\n },\n textBufferSetStyledText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n textBufferGetLineCount: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n textBufferGetPlainText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n textBufferAddHighlightByCharRange: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferAddHighlight: {\n args: [\"ptr\", \"u32\", \"ptr\"],\n returns: \"void\",\n },\n textBufferRemoveHighlightsByRef: {\n args: [\"ptr\", \"u16\"],\n returns: \"void\",\n },\n textBufferClearLineHighlights: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n textBufferClearAllHighlights: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferSetSyntaxStyle: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferGetLineHighlightsPtr: {\n args: [\"ptr\", \"u32\", \"ptr\"],\n returns: \"ptr\",\n },\n textBufferFreeLineHighlights: {\n args: [\"ptr\", \"usize\"],\n returns: \"void\",\n },\n textBufferGetHighlightCount: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n textBufferGetTextRange: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n textBufferGetTextRangeByCoords: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n\n // TextBufferView functions\n createTextBufferView: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n destroyTextBufferView: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferViewSetSelection: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferViewResetSelection: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferViewGetSelectionInfo: {\n args: [\"ptr\"],\n returns: \"u64\",\n },\n textBufferViewSetLocalSelection: {\n args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\"],\n returns: \"bool\",\n },\n textBufferViewUpdateSelection: {\n args: [\"ptr\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferViewUpdateLocalSelection: {\n args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\"],\n returns: \"bool\",\n },\n textBufferViewResetLocalSelection: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n textBufferViewSetWrapWidth: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n textBufferViewSetWrapMode: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n textBufferViewSetViewportSize: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n textBufferViewSetViewport: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n textBufferViewGetVirtualLineCount: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n textBufferViewGetLineInfoDirect: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferViewGetLogicalLineInfoDirect: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferViewGetSelectedText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n textBufferViewGetPlainText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n textBufferViewSetTabIndicator: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n textBufferViewSetTabIndicatorColor: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n textBufferViewSetTruncate: {\n args: [\"ptr\", \"bool\"],\n returns: \"void\",\n },\n textBufferViewMeasureForDimensions: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\"],\n returns: \"bool\",\n },\n bufferDrawTextBufferView: {\n args: [\"ptr\", \"ptr\", \"i32\", \"i32\"],\n returns: \"void\",\n },\n bufferDrawEditorView: {\n args: [\"ptr\", \"ptr\", \"i32\", \"i32\"],\n returns: \"void\",\n },\n\n // EditorView functions\n createEditorView: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"ptr\",\n },\n destroyEditorView: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewSetViewportSize: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n editorViewSetViewport: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"bool\"],\n returns: \"void\",\n },\n editorViewGetViewport: {\n args: [\"ptr\", \"ptr\", \"ptr\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewSetScrollMargin: {\n args: [\"ptr\", \"f32\"],\n returns: \"void\",\n },\n editorViewSetWrapMode: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n editorViewGetVirtualLineCount: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n editorViewGetTotalVirtualLineCount: {\n args: [\"ptr\"],\n returns: \"u32\",\n },\n editorViewGetTextBufferView: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n editorViewGetLineInfoDirect: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetLogicalLineInfoDirect: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n\n // EditBuffer functions\n createEditBuffer: {\n args: [\"u8\"],\n returns: \"ptr\",\n },\n destroyEditBuffer: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferSetText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n editBufferSetTextFromMem: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n editBufferReplaceText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n editBufferReplaceTextFromMem: {\n args: [\"ptr\", \"u8\"],\n returns: \"void\",\n },\n editBufferGetText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n editBufferInsertChar: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n editBufferInsertText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n editBufferDeleteChar: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferDeleteCharBackward: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferDeleteRange: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n editBufferNewLine: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferDeleteLine: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferMoveCursorLeft: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferMoveCursorRight: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferMoveCursorUp: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferMoveCursorDown: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferGotoLine: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n editBufferSetCursor: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n editBufferSetCursorToLineCol: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"void\",\n },\n editBufferSetCursorByOffset: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n editBufferGetCursorPosition: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editBufferGetId: {\n args: [\"ptr\"],\n returns: \"u16\",\n },\n editBufferGetTextBuffer: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n editBufferDebugLogRope: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferUndo: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n editBufferRedo: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n editBufferCanUndo: {\n args: [\"ptr\"],\n returns: \"bool\",\n },\n editBufferCanRedo: {\n args: [\"ptr\"],\n returns: \"bool\",\n },\n editBufferClearHistory: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferClear: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editBufferGetNextWordBoundary: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editBufferGetPrevWordBoundary: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editBufferGetEOL: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editBufferOffsetToPosition: {\n args: [\"ptr\", \"u32\", \"ptr\"],\n returns: \"bool\",\n },\n editBufferPositionToOffset: {\n args: [\"ptr\", \"u32\", \"u32\"],\n returns: \"u32\",\n },\n editBufferGetLineStartOffset: {\n args: [\"ptr\", \"u32\"],\n returns: \"u32\",\n },\n editBufferGetTextRange: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n editBufferGetTextRangeByCoords: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"u32\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n\n // EditorView selection and editing methods\n editorViewSetSelection: {\n args: [\"ptr\", \"u32\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewResetSelection: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewGetSelection: {\n args: [\"ptr\"],\n returns: \"u64\",\n },\n editorViewSetLocalSelection: {\n args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\", \"bool\", \"bool\"],\n returns: \"bool\",\n },\n editorViewUpdateSelection: {\n args: [\"ptr\", \"u32\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewUpdateLocalSelection: {\n args: [\"ptr\", \"i32\", \"i32\", \"i32\", \"i32\", \"ptr\", \"ptr\", \"bool\", \"bool\"],\n returns: \"bool\",\n },\n editorViewResetLocalSelection: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewGetSelectedTextBytes: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n editorViewGetCursor: {\n args: [\"ptr\", \"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"usize\",\n },\n\n // EditorView VisualCursor methods\n editorViewGetVisualCursor: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n\n editorViewMoveUpVisual: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewMoveDownVisual: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewDeleteSelectedText: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n editorViewSetCursorByOffset: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n editorViewGetNextWordBoundary: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetPrevWordBoundary: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetEOL: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetVisualSOL: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewGetVisualEOL: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n editorViewSetPlaceholderStyledText: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n editorViewSetTabIndicator: {\n args: [\"ptr\", \"u32\"],\n returns: \"void\",\n },\n editorViewSetTabIndicatorColor: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n\n getArenaAllocatedBytes: {\n args: [],\n returns: \"usize\",\n },\n getBuildOptions: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n getAllocatorStats: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n\n // SyntaxStyle functions\n createSyntaxStyle: {\n args: [],\n returns: \"ptr\",\n },\n destroySyntaxStyle: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n syntaxStyleRegister: {\n args: [\"ptr\", \"ptr\", \"usize\", \"ptr\", \"ptr\", \"u8\"],\n returns: \"u32\",\n },\n syntaxStyleResolveByName: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"u32\",\n },\n syntaxStyleGetStyleCount: {\n args: [\"ptr\"],\n returns: \"usize\",\n },\n\n // Terminal capability functions\n getTerminalCapabilities: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n processCapabilityResponse: {\n args: [\"ptr\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n\n // Unicode encoding API\n encodeUnicode: {\n args: [\"ptr\", \"usize\", \"ptr\", \"ptr\", \"u8\"],\n returns: \"bool\",\n },\n freeUnicode: {\n args: [\"ptr\", \"usize\"],\n returns: \"void\",\n },\n bufferDrawChar: {\n args: [\"ptr\", \"u32\", \"u32\", \"u32\", \"ptr\", \"ptr\", \"u32\"],\n returns: \"void\",\n },\n\n // NativeSpanFeed\n createNativeSpanFeed: {\n args: [\"ptr\"],\n returns: \"ptr\",\n },\n attachNativeSpanFeed: {\n args: [\"ptr\"],\n returns: \"i32\",\n },\n destroyNativeSpanFeed: {\n args: [\"ptr\"],\n returns: \"void\",\n },\n streamWrite: {\n args: [\"ptr\", \"ptr\", \"u64\"],\n returns: \"i32\",\n },\n streamCommit: {\n args: [\"ptr\"],\n returns: \"i32\",\n },\n streamDrainSpans: {\n args: [\"ptr\", \"ptr\", \"u32\"],\n returns: \"u32\",\n },\n streamClose: {\n args: [\"ptr\"],\n returns: \"i32\",\n },\n streamReserve: {\n args: [\"ptr\", \"u32\", \"ptr\"],\n returns: \"i32\",\n },\n streamCommitReserved: {\n args: [\"ptr\", \"u32\"],\n returns: \"i32\",\n },\n streamSetOptions: {\n args: [\"ptr\", \"ptr\"],\n returns: \"i32\",\n },\n streamGetStats: {\n args: [\"ptr\", \"ptr\"],\n returns: \"i32\",\n },\n streamSetCallback: {\n args: [\"ptr\", \"ptr\"],\n returns: \"void\",\n },\n })\n\n if (env.OTUI_DEBUG_FFI || env.OTUI_TRACE_FFI) {\n return {\n symbols: convertToDebugSymbols(rawSymbols.symbols),\n }\n }\n\n return rawSymbols\n}\n\nfunction convertToDebugSymbols<T extends Record<string, any>>(symbols: T): T {\n // Initialize global state on first call\n if (!globalTraceSymbols) {\n globalTraceSymbols = {}\n }\n\n // Initialize global debug log path on first call\n if (env.OTUI_DEBUG_FFI && !globalFFILogPath) {\n const now = new Date()\n const timestamp = now.toISOString().replace(/[:.]/g, \"-\").replace(/T/, \"_\").split(\"Z\")[0]\n globalFFILogPath = `ffi_otui_debug_${timestamp}.log`\n }\n\n const debugSymbols: Record<string, any> = {}\n let hasTracing = false\n\n Object.entries(symbols).forEach(([key, value]) => {\n debugSymbols[key] = value\n })\n\n if (env.OTUI_DEBUG_FFI && globalFFILogPath) {\n const logPath = globalFFILogPath\n const writeSync = (msg: string) => {\n writeFileSync(logPath, msg + \"\\n\", { flag: \"a\" })\n }\n\n Object.entries(symbols).forEach(([key, value]) => {\n if (typeof value === \"function\") {\n debugSymbols[key] = (...args: any[]) => {\n writeSync(`${key}(${args.map((arg) => String(arg)).join(\", \")})`)\n const result = value(...args)\n writeSync(`${key} returned: ${String(result)}`)\n return result\n }\n }\n })\n }\n\n if (env.OTUI_TRACE_FFI) {\n hasTracing = true\n Object.entries(symbols).forEach(([key, value]) => {\n if (typeof value === \"function\") {\n // Initialize trace array for this symbol if not exists\n if (!globalTraceSymbols![key]) {\n globalTraceSymbols![key] = []\n }\n\n const originalFunc = debugSymbols[key]\n debugSymbols[key] = (...args: any[]) => {\n const start = performance.now()\n const result = originalFunc(...args)\n const end = performance.now()\n globalTraceSymbols![key].push(end - start)\n return result\n }\n }\n })\n }\n\n // Register exit handler only once\n if ((env.OTUI_DEBUG_FFI || env.OTUI_TRACE_FFI) && !exitHandlerRegistered) {\n exitHandlerRegistered = true\n\n process.on(\"exit\", () => {\n if (globalTraceSymbols) {\n const allStats: Array<{\n name: string\n count: number\n total: number\n average: number\n min: number\n max: number\n median: number\n p90: number\n p99: number\n }> = []\n\n for (const [key, timings] of Object.entries(globalTraceSymbols)) {\n if (!Array.isArray(timings) || timings.length === 0) {\n continue\n }\n\n const sortedTimings = [...timings].sort((a, b) => a - b)\n const count = sortedTimings.length\n\n const total = sortedTimings.reduce((acc, t) => acc + t, 0)\n const average = total / count\n const min = sortedTimings[0]\n const max = sortedTimings[count - 1]\n\n const medianIndex = Math.floor(count / 2)\n const p90Index = Math.floor(count * 0.9)\n const p99Index = Math.floor(count * 0.99)\n\n const median = sortedTimings[medianIndex]\n const p90 = sortedTimings[Math.min(p90Index, count - 1)]\n const p99 = sortedTimings[Math.min(p99Index, count - 1)]\n\n allStats.push({\n name: key,\n count,\n total,\n average,\n min,\n max,\n median,\n p90,\n p99,\n })\n }\n\n allStats.sort((a, b) => b.total - a.total)\n\n const lines: string[] = []\n lines.push(\"\\n--- OpenTUI FFI Call Performance ---\")\n lines.push(\"Sorted by total time spent (descending)\")\n lines.push(\n \"-------------------------------------------------------------------------------------------------------------------------\",\n )\n\n if (allStats.length === 0) {\n lines.push(\"No trace data collected or all symbols had zero calls.\")\n } else {\n const nameHeader = \"Symbol\"\n const callsHeader = \"Calls\"\n const totalHeader = \"Total (ms)\"\n const avgHeader = \"Avg (ms)\"\n const minHeader = \"Min (ms)\"\n const maxHeader = \"Max (ms)\"\n const medHeader = \"Med (ms)\"\n const p90Header = \"P90 (ms)\"\n const p99Header = \"P99 (ms)\"\n\n const nameWidth = Math.max(nameHeader.length, ...allStats.map((s) => s.name.length))\n const countWidth = Math.max(callsHeader.length, ...allStats.map((s) => String(s.count).length))\n const totalWidth = Math.max(totalHeader.length, ...allStats.map((s) => s.total.toFixed(2).length))\n const avgWidth = Math.max(avgHeader.length, ...allStats.map((s) => s.average.toFixed(2).length))\n const statWidthMin = Math.max(minHeader.length, ...allStats.map((s) => s.min.toFixed(2).length))\n const statWidthMax = Math.max(maxHeader.length, ...allStats.map((s) => s.max.toFixed(2).length))\n const medianWidth = Math.max(medHeader.length, ...allStats.map((s) => s.median.toFixed(2).length))\n const p90Width = Math.max(p90Header.length, ...allStats.map((s) => s.p90.toFixed(2).length))\n const p99Width = Math.max(p99Header.length, ...allStats.map((s) => s.p99.toFixed(2).length))\n\n lines.push(\n `${nameHeader.padEnd(nameWidth)} | ` +\n `${callsHeader.padStart(countWidth)} | ` +\n `${totalHeader.padStart(totalWidth)} | ` +\n `${avgHeader.padStart(avgWidth)} | ` +\n `${minHeader.padStart(statWidthMin)} | ` +\n `${maxHeader.padStart(statWidthMax)} | ` +\n `${medHeader.padStart(medianWidth)} | ` +\n `${p90Header.padStart(p90Width)} | ` +\n `${p99Header.padStart(p99Width)}`,\n )\n lines.push(\n `${\"-\".repeat(nameWidth)}-+-${\"-\".repeat(countWidth)}-+-${\"-\".repeat(totalWidth)}-+-${\"-\".repeat(avgWidth)}-+-${\"-\".repeat(statWidthMin)}-+-${\"-\".repeat(statWidthMax)}-+-${\"-\".repeat(medianWidth)}-+-${\"-\".repeat(p90Width)}-+-${\"-\".repeat(p99Width)}`,\n )\n\n allStats.forEach((stat) => {\n lines.push(\n `${stat.name.padEnd(nameWidth)} | ` +\n `${String(stat.count).padStart(countWidth)} | ` +\n `${stat.total.toFixed(2).padStart(totalWidth)} | ` +\n `${stat.average.toFixed(2).padStart(avgWidth)} | ` +\n `${stat.min.toFixed(2).padStart(statWidthMin)} | ` +\n `${stat.max.toFixed(2).padStart(statWidthMax)} | ` +\n `${stat.median.toFixed(2).padStart(medianWidth)} | ` +\n `${stat.p90.toFixed(2).padStart(p90Width)} | ` +\n `${stat.p99.toFixed(2).padStart(p99Width)}`,\n )\n })\n }\n lines.push(\n \"-------------------------------------------------------------------------------------------------------------------------\",\n )\n\n const output = lines.join(\"\\n\")\n console.log(output)\n\n try {\n const now = new Date()\n const timestamp = now.toISOString().replace(/[:.]/g, \"-\").replace(/T/, \"_\").split(\"Z\")[0]\n const traceFilePath = `ffi_otui_trace_${timestamp}.log`\n Bun.write(traceFilePath, output)\n } catch (e) {\n console.error(\"Failed to write FFI trace file:\", e)\n }\n }\n })\n }\n\n return debugSymbols as T\n}\n\n// Log levels matching Zig's LogLevel enum\nexport enum LogLevel {\n Error = 0,\n Warn = 1,\n Info = 2,\n Debug = 3,\n}\n\n/**\n * VisualCursor represents a cursor position with both visual and logical coordinates.\n * Visual coordinates (visualRow, visualCol) are VIEWPORT-RELATIVE.\n * This means visualRow=0 is the first visible line in the viewport, not the first line in the document.\n * Logical coordinates (logicalRow, logicalCol) are document-absolute.\n */\nexport interface VisualCursor {\n visualRow: number // Viewport-relative row (0 = top of viewport)\n visualCol: number // Viewport-relative column (0 = left edge of viewport when not wrapping)\n logicalRow: number // Document-absolute row\n logicalCol: number // Document-absolute column\n offset: number // Global display-width offset from buffer start\n}\n\nexport interface LogicalCursor {\n row: number\n col: number\n offset: number\n}\n\nexport interface CursorState {\n x: number\n y: number\n visible: boolean\n style: CursorStyle\n blinking: boolean\n color: RGBA\n}\n\nexport type NativeSpanFeedEventHandler = (eventId: number, arg0: Pointer, arg1: number | bigint) => void\n\nexport interface RenderLib {\n createRenderer: (width: number, height: number, options?: { testing?: boolean; remote?: boolean }) => Pointer | null\n setTerminalEnvVar: (renderer: Pointer, key: string, value: string) => boolean\n destroyRenderer: (renderer: Pointer) => void\n setUseThread: (renderer: Pointer, useThread: boolean) => void\n setBackgroundColor: (renderer: Pointer, color: RGBA) => void\n setRenderOffset: (renderer: Pointer, offset: number) => void\n updateStats: (renderer: Pointer, time: number, fps: number, frameCallbackTime: number) => void\n updateMemoryStats: (renderer: Pointer, heapUsed: number, heapTotal: number, arrayBuffers: number) => void\n render: (renderer: Pointer, force: boolean) => void\n getNextBuffer: (renderer: Pointer) => OptimizedBuffer\n getCurrentBuffer: (renderer: Pointer) => OptimizedBuffer\n createOptimizedBuffer: (\n width: number,\n height: number,\n widthMethod: WidthMethod,\n respectAlpha?: boolean,\n id?: string,\n ) => OptimizedBuffer\n destroyOptimizedBuffer: (bufferPtr: Pointer) => void\n drawFrameBuffer: (\n targetBufferPtr: Pointer,\n destX: number,\n destY: number,\n bufferPtr: Pointer,\n sourceX?: number,\n sourceY?: number,\n sourceWidth?: number,\n sourceHeight?: number,\n ) => void\n getBufferWidth: (buffer: Pointer) => number\n getBufferHeight: (buffer: Pointer) => number\n bufferClear: (buffer: Pointer, color: RGBA) => void\n bufferGetCharPtr: (buffer: Pointer) => Pointer\n bufferGetFgPtr: (buffer: Pointer) => Pointer\n bufferGetBgPtr: (buffer: Pointer) => Pointer\n bufferGetAttributesPtr: (buffer: Pointer) => Pointer\n bufferGetRespectAlpha: (buffer: Pointer) => boolean\n bufferSetRespectAlpha: (buffer: Pointer, respectAlpha: boolean) => void\n bufferGetId: (buffer: Pointer) => string\n bufferGetRealCharSize: (buffer: Pointer) => number\n bufferWriteResolvedChars: (buffer: Pointer, outputBuffer: Uint8Array, addLineBreaks: boolean) => number\n bufferDrawText: (\n buffer: Pointer,\n text: string,\n x: number,\n y: number,\n color: RGBA,\n bgColor?: RGBA,\n attributes?: number,\n ) => void\n bufferSetCellWithAlphaBlending: (\n buffer: Pointer,\n x: number,\n y: number,\n char: string,\n color: RGBA,\n bgColor: RGBA,\n attributes?: number,\n ) => void\n bufferSetCell: (\n buffer: Pointer,\n x: number,\n y: number,\n char: string,\n color: RGBA,\n bgColor: RGBA,\n attributes?: number,\n ) => void\n bufferFillRect: (buffer: Pointer, x: number, y: number, width: number, height: number, color: RGBA) => void\n bufferColorMatrix: (\n buffer: Pointer,\n matrixPtr: Pointer,\n cellMaskPtr: Pointer,\n cellMaskCount: number,\n strength: number,\n target: TargetChannel,\n ) => void\n bufferColorMatrixUniform: (buffer: Pointer, matrixPtr: Pointer, strength: number, target: TargetChannel) => void\n bufferDrawSuperSampleBuffer: (\n buffer: Pointer,\n x: number,\n y: number,\n pixelDataPtr: Pointer,\n pixelDataLength: number,\n format: \"bgra8unorm\" | \"rgba8unorm\",\n alignedBytesPerRow: number,\n ) => void\n bufferDrawPackedBuffer: (\n buffer: Pointer,\n dataPtr: Pointer,\n dataLen: number,\n posX: number,\n posY: number,\n terminalWidthCells: number,\n terminalHeightCells: number,\n ) => void\n bufferDrawGrayscaleBuffer: (\n buffer: Pointer,\n posX: number,\n posY: number,\n intensitiesPtr: Pointer,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null,\n bg: RGBA | null,\n ) => void\n bufferDrawGrayscaleBufferSupersampled: (\n buffer: Pointer,\n posX: number,\n posY: number,\n intensitiesPtr: Pointer,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null,\n bg: RGBA | null,\n ) => void\n bufferDrawGrid: (\n buffer: Pointer,\n borderChars: Uint32Array,\n borderFg: RGBA,\n borderBg: RGBA,\n columnOffsets: Int32Array,\n columnCount: number,\n rowOffsets: Int32Array,\n rowCount: number,\n options: { drawInner: boolean; drawOuter: boolean },\n ) => void\n bufferDrawBox: (\n buffer: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n borderChars: Uint32Array,\n packedOptions: number,\n borderColor: RGBA,\n backgroundColor: RGBA,\n title: string | null,\n ) => void\n bufferResize: (buffer: Pointer, width: number, height: number) => void\n resizeRenderer: (renderer: Pointer, width: number, height: number) => void\n setCursorPosition: (renderer: Pointer, x: number, y: number, visible: boolean) => void\n setCursorColor: (renderer: Pointer, color: RGBA) => void\n getCursorState: (renderer: Pointer) => CursorState\n setCursorStyleOptions: (renderer: Pointer, options: CursorStyleOptions) => void\n setDebugOverlay: (renderer: Pointer, enabled: boolean, corner: DebugOverlayCorner) => void\n clearTerminal: (renderer: Pointer) => void\n setTerminalTitle: (renderer: Pointer, title: string) => void\n copyToClipboardOSC52: (renderer: Pointer, target: number, payload: Uint8Array) => boolean\n clearClipboardOSC52: (renderer: Pointer, target: number) => boolean\n addToHitGrid: (renderer: Pointer, x: number, y: number, width: number, height: number, id: number) => void\n clearCurrentHitGrid: (renderer: Pointer) => void\n hitGridPushScissorRect: (renderer: Pointer, x: number, y: number, width: number, height: number) => void\n hitGridPopScissorRect: (renderer: Pointer) => void\n hitGridClearScissorRects: (renderer: Pointer) => void\n addToCurrentHitGridClipped: (\n renderer: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n id: number,\n ) => void\n checkHit: (renderer: Pointer, x: number, y: number) => number\n getHitGridDirty: (renderer: Pointer) => boolean\n dumpHitGrid: (renderer: Pointer) => void\n dumpBuffers: (renderer: Pointer, timestamp?: number) => void\n dumpStdoutBuffer: (renderer: Pointer, timestamp?: number) => void\n restoreTerminalModes: (renderer: Pointer) => void\n enableMouse: (renderer: Pointer, enableMovement: boolean) => void\n disableMouse: (renderer: Pointer) => void\n enableKittyKeyboard: (renderer: Pointer, flags: number) => void\n disableKittyKeyboard: (renderer: Pointer) => void\n setKittyKeyboardFlags: (renderer: Pointer, flags: number) => void\n getKittyKeyboardFlags: (renderer: Pointer) => number\n setupTerminal: (renderer: Pointer, useAlternateScreen: boolean) => void\n suspendRenderer: (renderer: Pointer) => void\n resumeRenderer: (renderer: Pointer) => void\n queryPixelResolution: (renderer: Pointer) => void\n writeOut: (renderer: Pointer, data: string | Uint8Array) => void\n\n // TextBuffer methods\n createTextBuffer: (widthMethod: WidthMethod) => TextBuffer\n destroyTextBuffer: (buffer: Pointer) => void\n textBufferGetLength: (buffer: Pointer) => number\n textBufferGetByteSize: (buffer: Pointer) => number\n\n textBufferReset: (buffer: Pointer) => void\n textBufferClear: (buffer: Pointer) => void\n textBufferRegisterMemBuffer: (buffer: Pointer, bytes: Uint8Array, owned?: boolean) => number\n textBufferReplaceMemBuffer: (buffer: Pointer, memId: number, bytes: Uint8Array, owned?: boolean) => boolean\n textBufferClearMemRegistry: (buffer: Pointer) => void\n textBufferSetTextFromMem: (buffer: Pointer, memId: number) => void\n textBufferAppend: (buffer: Pointer, bytes: Uint8Array) => void\n textBufferAppendFromMemId: (buffer: Pointer, memId: number) => void\n textBufferLoadFile: (buffer: Pointer, path: string) => boolean\n textBufferSetStyledText: (\n buffer: Pointer,\n chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number; link?: { url: string } }>,\n ) => void\n textBufferSetDefaultFg: (buffer: Pointer, fg: RGBA | null) => void\n textBufferSetDefaultBg: (buffer: Pointer, bg: RGBA | null) => void\n textBufferSetDefaultAttributes: (buffer: Pointer, attributes: number | null) => void\n textBufferResetDefaults: (buffer: Pointer) => void\n textBufferGetTabWidth: (buffer: Pointer) => number\n textBufferSetTabWidth: (buffer: Pointer, width: number) => void\n textBufferGetLineCount: (buffer: Pointer) => number\n getPlainTextBytes: (buffer: Pointer, maxLength: number) => Uint8Array | null\n textBufferGetTextRange: (\n buffer: Pointer,\n startOffset: number,\n endOffset: number,\n maxLength: number,\n ) => Uint8Array | null\n textBufferGetTextRangeByCoords: (\n buffer: Pointer,\n startRow: number,\n startCol: number,\n endRow: number,\n endCol: number,\n maxLength: number,\n ) => Uint8Array | null\n\n // TextBufferView methods\n createTextBufferView: (textBuffer: Pointer) => Pointer\n destroyTextBufferView: (view: Pointer) => void\n textBufferViewSetSelection: (\n view: Pointer,\n start: number,\n end: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ) => void\n textBufferViewResetSelection: (view: Pointer) => void\n textBufferViewGetSelection: (view: Pointer) => { start: number; end: number } | null\n textBufferViewSetLocalSelection: (\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ) => boolean\n textBufferViewUpdateSelection: (view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null) => void\n textBufferViewUpdateLocalSelection: (\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ) => boolean\n textBufferViewResetLocalSelection: (view: Pointer) => void\n textBufferViewSetWrapWidth: (view: Pointer, width: number) => void\n textBufferViewSetWrapMode: (view: Pointer, mode: \"none\" | \"char\" | \"word\") => void\n textBufferViewSetViewportSize: (view: Pointer, width: number, height: number) => void\n textBufferViewSetViewport: (view: Pointer, x: number, y: number, width: number, height: number) => void\n textBufferViewGetLineInfo: (view: Pointer) => LineInfo\n textBufferViewGetLogicalLineInfo: (view: Pointer) => LineInfo\n textBufferViewGetSelectedTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n textBufferViewGetPlainTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n textBufferViewSetTabIndicator: (view: Pointer, indicator: number) => void\n textBufferViewSetTabIndicatorColor: (view: Pointer, color: RGBA) => void\n textBufferViewSetTruncate: (view: Pointer, truncate: boolean) => void\n textBufferViewMeasureForDimensions: (\n view: Pointer,\n width: number,\n height: number,\n ) => { lineCount: number; widthColsMax: number } | null\n textBufferViewGetVirtualLineCount: (view: Pointer) => number\n\n readonly encoder: TextEncoder\n readonly decoder: TextDecoder\n bufferDrawTextBufferView: (buffer: Pointer, view: Pointer, x: number, y: number) => void\n bufferDrawEditorView: (buffer: Pointer, view: Pointer, x: number, y: number) => void\n\n // EditBuffer methods\n createEditBuffer: (widthMethod: WidthMethod) => Pointer\n destroyEditBuffer: (buffer: Pointer) => void\n editBufferSetText: (buffer: Pointer, textBytes: Uint8Array) => void\n editBufferSetTextFromMem: (buffer: Pointer, memId: number) => void\n editBufferReplaceText: (buffer: Pointer, textBytes: Uint8Array) => void\n editBufferReplaceTextFromMem: (buffer: Pointer, memId: number) => void\n editBufferGetText: (buffer: Pointer, maxLength: number) => Uint8Array | null\n editBufferInsertChar: (buffer: Pointer, char: string) => void\n editBufferInsertText: (buffer: Pointer, text: string) => void\n editBufferDeleteChar: (buffer: Pointer) => void\n editBufferDeleteCharBackward: (buffer: Pointer) => void\n editBufferDeleteRange: (buffer: Pointer, startLine: number, startCol: number, endLine: number, endCol: number) => void\n editBufferNewLine: (buffer: Pointer) => void\n editBufferDeleteLine: (buffer: Pointer) => void\n editBufferMoveCursorLeft: (buffer: Pointer) => void\n editBufferMoveCursorRight: (buffer: Pointer) => void\n editBufferMoveCursorUp: (buffer: Pointer) => void\n editBufferMoveCursorDown: (buffer: Pointer) => void\n editBufferGotoLine: (buffer: Pointer, line: number) => void\n editBufferSetCursor: (buffer: Pointer, line: number, col: number) => void\n editBufferSetCursorToLineCol: (buffer: Pointer, line: number, col: number) => void\n editBufferSetCursorByOffset: (buffer: Pointer, offset: number) => void\n editBufferGetCursorPosition: (buffer: Pointer) => LogicalCursor\n editBufferGetId: (buffer: Pointer) => number\n editBufferGetTextBuffer: (buffer: Pointer) => Pointer\n editBufferDebugLogRope: (buffer: Pointer) => void\n editBufferUndo: (buffer: Pointer, maxLength: number) => Uint8Array | null\n editBufferRedo: (buffer: Pointer, maxLength: number) => Uint8Array | null\n editBufferCanUndo: (buffer: Pointer) => boolean\n editBufferCanRedo: (buffer: Pointer) => boolean\n editBufferClearHistory: (buffer: Pointer) => void\n editBufferClear: (buffer: Pointer) => void\n editBufferGetNextWordBoundary: (buffer: Pointer) => { row: number; col: number; offset: number }\n editBufferGetPrevWordBoundary: (buffer: Pointer) => { row: number; col: number; offset: number }\n editBufferGetEOL: (buffer: Pointer) => { row: number; col: number; offset: number }\n editBufferOffsetToPosition: (buffer: Pointer, offset: number) => { row: number; col: number; offset: number } | null\n editBufferPositionToOffset: (buffer: Pointer, row: number, col: number) => number\n editBufferGetLineStartOffset: (buffer: Pointer, row: number) => number\n editBufferGetTextRange: (\n buffer: Pointer,\n startOffset: number,\n endOffset: number,\n maxLength: number,\n ) => Uint8Array | null\n editBufferGetTextRangeByCoords: (\n buffer: Pointer,\n startRow: number,\n startCol: number,\n endRow: number,\n endCol: number,\n maxLength: number,\n ) => Uint8Array | null\n\n // EditorView methods\n createEditorView: (editBufferPtr: Pointer, viewportWidth: number, viewportHeight: number) => Pointer\n destroyEditorView: (view: Pointer) => void\n editorViewSetViewportSize: (view: Pointer, width: number, height: number) => void\n editorViewSetViewport: (\n view: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n moveCursor: boolean,\n ) => void\n editorViewGetViewport: (view: Pointer) => { offsetY: number; offsetX: number; height: number; width: number }\n editorViewSetScrollMargin: (view: Pointer, margin: number) => void\n editorViewSetWrapMode: (view: Pointer, mode: \"none\" | \"char\" | \"word\") => void\n editorViewGetVirtualLineCount: (view: Pointer) => number\n editorViewGetTotalVirtualLineCount: (view: Pointer) => number\n editorViewGetTextBufferView: (view: Pointer) => Pointer\n editorViewSetSelection: (\n view: Pointer,\n start: number,\n end: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ) => void\n editorViewResetSelection: (view: Pointer) => void\n editorViewGetSelection: (view: Pointer) => { start: number; end: number } | null\n editorViewSetLocalSelection: (\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n updateCursor: boolean,\n followCursor: boolean,\n ) => boolean\n\n editorViewUpdateSelection: (view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null) => void\n editorViewUpdateLocalSelection: (\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n updateCursor: boolean,\n followCursor: boolean,\n ) => boolean\n\n editorViewResetLocalSelection: (view: Pointer) => void\n editorViewGetSelectedTextBytes: (view: Pointer, maxLength: number) => Uint8Array | null\n editorViewGetCursor: (view: Pointer) => { row: number; col: number }\n editorViewGetText: (view: Pointer, maxLength: number) => Uint8Array | null\n editorViewGetVisualCursor: (view: Pointer) => VisualCursor\n editorViewMoveUpVisual: (view: Pointer) => void\n editorViewMoveDownVisual: (view: Pointer) => void\n editorViewDeleteSelectedText: (view: Pointer) => void\n editorViewSetCursorByOffset: (view: Pointer, offset: number) => void\n editorViewGetNextWordBoundary: (view: Pointer) => VisualCursor\n editorViewGetPrevWordBoundary: (view: Pointer) => VisualCursor\n editorViewGetEOL: (view: Pointer) => VisualCursor\n editorViewGetVisualSOL: (view: Pointer) => VisualCursor\n editorViewGetVisualEOL: (view: Pointer) => VisualCursor\n editorViewGetLineInfo: (view: Pointer) => LineInfo\n editorViewGetLogicalLineInfo: (view: Pointer) => LineInfo\n editorViewSetPlaceholderStyledText: (\n view: Pointer,\n chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number }>,\n ) => void\n editorViewSetTabIndicator: (view: Pointer, indicator: number) => void\n editorViewSetTabIndicatorColor: (view: Pointer, color: RGBA) => void\n\n bufferPushScissorRect: (buffer: Pointer, x: number, y: number, width: number, height: number) => void\n bufferPopScissorRect: (buffer: Pointer) => void\n bufferClearScissorRects: (buffer: Pointer) => void\n bufferPushOpacity: (buffer: Pointer, opacity: number) => void\n bufferPopOpacity: (buffer: Pointer) => void\n bufferGetCurrentOpacity: (buffer: Pointer) => number\n bufferClearOpacity: (buffer: Pointer) => void\n textBufferAddHighlightByCharRange: (buffer: Pointer, highlight: Highlight) => void\n textBufferAddHighlight: (buffer: Pointer, lineIdx: number, highlight: Highlight) => void\n textBufferRemoveHighlightsByRef: (buffer: Pointer, hlRef: number) => void\n textBufferClearLineHighlights: (buffer: Pointer, lineIdx: number) => void\n textBufferClearAllHighlights: (buffer: Pointer) => void\n textBufferSetSyntaxStyle: (buffer: Pointer, style: Pointer | null) => void\n textBufferGetLineHighlights: (buffer: Pointer, lineIdx: number) => Array<Highlight>\n textBufferGetHighlightCount: (buffer: Pointer) => number\n\n getArenaAllocatedBytes: () => number\n getBuildOptions: () => BuildOptions\n getAllocatorStats: () => AllocatorStats\n\n createSyntaxStyle: () => Pointer\n destroySyntaxStyle: (style: Pointer) => void\n syntaxStyleRegister: (style: Pointer, name: string, fg: RGBA | null, bg: RGBA | null, attributes: number) => number\n syntaxStyleResolveByName: (style: Pointer, name: string) => number | null\n syntaxStyleGetStyleCount: (style: Pointer) => number\n\n getTerminalCapabilities: (renderer: Pointer) => any\n processCapabilityResponse: (renderer: Pointer, response: string) => void\n\n encodeUnicode: (\n text: string,\n widthMethod: WidthMethod,\n ) => { ptr: Pointer; data: Array<{ width: number; char: number }> } | null\n freeUnicode: (encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }) => void\n bufferDrawChar: (buffer: Pointer, char: number, x: number, y: number, fg: RGBA, bg: RGBA, attributes?: number) => void\n\n registerNativeSpanFeedStream: (stream: Pointer, handler: NativeSpanFeedEventHandler) => void\n unregisterNativeSpanFeedStream: (stream: Pointer) => void\n createNativeSpanFeed: (options?: NativeSpanFeedOptions | null) => Pointer\n attachNativeSpanFeed: (stream: Pointer) => number\n destroyNativeSpanFeed: (stream: Pointer) => void\n streamWrite: (stream: Pointer, data: Uint8Array | string) => number\n streamCommit: (stream: Pointer) => number\n streamDrainSpans: (stream: Pointer, outBuffer: Uint8Array, maxSpans: number) => number\n streamClose: (stream: Pointer) => number\n streamSetOptions: (stream: Pointer, options: NativeSpanFeedOptions) => number\n streamGetStats: (stream: Pointer) => NativeSpanFeedStats | null\n streamReserve: (stream: Pointer, minLen: number) => { status: number; info: ReserveInfo | null }\n streamCommitReserved: (stream: Pointer, length: number) => number\n\n onNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n onceNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n offNativeEvent: (name: string, handler: (data: ArrayBuffer) => void) => void\n onAnyNativeEvent: (handler: (name: string, data: ArrayBuffer) => void) => void\n}\n\nclass FFIRenderLib implements RenderLib {\n private opentui: ReturnType<typeof getOpenTUILib>\n public readonly encoder: TextEncoder = new TextEncoder()\n public readonly decoder: TextDecoder = new TextDecoder()\n private logCallbackWrapper: any // Store the FFI callback wrapper\n private eventCallbackWrapper: any // Store the FFI event callback wrapper\n private _nativeEvents: EventEmitter = new EventEmitter()\n private _anyEventHandlers: Array<(name: string, data: ArrayBuffer) => void> = []\n private nativeSpanFeedCallbackWrapper: JSCallback | null = null\n private nativeSpanFeedHandlers = new Map<Pointer, NativeSpanFeedEventHandler>()\n\n constructor(libPath?: string) {\n this.opentui = getOpenTUILib(libPath)\n this.setupLogging()\n this.setupEventBus()\n }\n\n private setupLogging() {\n if (this.logCallbackWrapper) {\n return\n }\n\n const logCallback = new JSCallback(\n (level: number, msgPtr: Pointer, msgLenBigInt: bigint | number) => {\n try {\n const msgLen = typeof msgLenBigInt === \"bigint\" ? Number(msgLenBigInt) : msgLenBigInt\n\n if (msgLen === 0 || !msgPtr) {\n return\n }\n\n const msgBuffer = toArrayBuffer(msgPtr, 0, msgLen)\n const msgBytes = new Uint8Array(msgBuffer)\n const message = this.decoder.decode(msgBytes)\n\n switch (level) {\n case LogLevel.Error:\n console.error(message)\n break\n case LogLevel.Warn:\n console.warn(message)\n break\n case LogLevel.Info:\n console.info(message)\n break\n case LogLevel.Debug:\n console.debug(message)\n break\n default:\n console.log(message)\n }\n } catch (error) {\n console.error(\"Error in Zig log callback:\", error)\n }\n },\n {\n args: [\"u8\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n )\n\n this.logCallbackWrapper = logCallback\n\n if (!logCallback.ptr) {\n throw new Error(\"Failed to create log callback\")\n }\n\n this.setLogCallback(logCallback.ptr)\n }\n\n private setLogCallback(callbackPtr: Pointer) {\n this.opentui.symbols.setLogCallback(callbackPtr)\n }\n\n private setupEventBus() {\n if (this.eventCallbackWrapper) {\n return\n }\n\n const eventCallback = new JSCallback(\n (namePtr: Pointer, nameLenBigInt: bigint | number, dataPtr: Pointer, dataLenBigInt: bigint | number) => {\n try {\n const nameLen = typeof nameLenBigInt === \"bigint\" ? Number(nameLenBigInt) : nameLenBigInt\n const dataLen = typeof dataLenBigInt === \"bigint\" ? Number(dataLenBigInt) : dataLenBigInt\n\n if (nameLen === 0 || !namePtr) {\n return\n }\n\n const nameBuffer = toArrayBuffer(namePtr, 0, nameLen)\n const nameBytes = new Uint8Array(nameBuffer)\n const eventName = this.decoder.decode(nameBytes)\n\n let eventData: ArrayBuffer\n if (dataLen > 0 && dataPtr) {\n eventData = toArrayBuffer(dataPtr, 0, dataLen).slice()\n } else {\n eventData = new ArrayBuffer(0)\n }\n\n queueMicrotask(() => {\n this._nativeEvents.emit(eventName, eventData)\n\n for (const handler of this._anyEventHandlers) {\n handler(eventName, eventData)\n }\n })\n } catch (error) {\n console.error(\"Error in native event callback:\", error)\n }\n },\n {\n args: [\"ptr\", \"usize\", \"ptr\", \"usize\"],\n returns: \"void\",\n },\n )\n\n this.eventCallbackWrapper = eventCallback\n\n if (!eventCallback.ptr) {\n throw new Error(\"Failed to create event callback\")\n }\n\n this.setEventCallback(eventCallback.ptr)\n }\n\n private ensureNativeSpanFeedCallback(): JSCallback {\n if (this.nativeSpanFeedCallbackWrapper) {\n return this.nativeSpanFeedCallbackWrapper\n }\n\n const callback = new JSCallback(\n (streamPtr: Pointer, eventId: number, arg0: Pointer, arg1: number | bigint) => {\n const handler = this.nativeSpanFeedHandlers.get(toPointer(streamPtr))\n if (handler) {\n handler(eventId, arg0, arg1)\n }\n },\n {\n args: [\"ptr\", \"u32\", \"ptr\", \"u64\"],\n returns: \"void\",\n },\n )\n\n this.nativeSpanFeedCallbackWrapper = callback\n\n if (!callback.ptr) {\n throw new Error(\"Failed to create native span feed callback\")\n }\n\n return callback\n }\n\n private setEventCallback(callbackPtr: Pointer) {\n this.opentui.symbols.setEventCallback(callbackPtr)\n }\n\n public createRenderer(width: number, height: number, options: { testing?: boolean; remote?: boolean } = {}) {\n const testing = options.testing ?? false\n const remote = options.remote ?? false\n return this.opentui.symbols.createRenderer(width, height, testing, remote)\n }\n\n public setTerminalEnvVar(renderer: Pointer, key: string, value: string): boolean {\n const keyBytes = this.encoder.encode(key)\n const valueBytes = this.encoder.encode(value)\n return this.opentui.symbols.setTerminalEnvVar(renderer, keyBytes, keyBytes.length, valueBytes, valueBytes.length)\n }\n\n public destroyRenderer(renderer: Pointer): void {\n this.opentui.symbols.destroyRenderer(renderer)\n }\n\n public setUseThread(renderer: Pointer, useThread: boolean) {\n this.opentui.symbols.setUseThread(renderer, useThread)\n }\n\n public setBackgroundColor(renderer: Pointer, color: RGBA) {\n this.opentui.symbols.setBackgroundColor(renderer, color.buffer)\n }\n\n public setRenderOffset(renderer: Pointer, offset: number) {\n this.opentui.symbols.setRenderOffset(renderer, offset)\n }\n\n public updateStats(renderer: Pointer, time: number, fps: number, frameCallbackTime: number) {\n this.opentui.symbols.updateStats(renderer, time, fps, frameCallbackTime)\n }\n\n public updateMemoryStats(renderer: Pointer, heapUsed: number, heapTotal: number, arrayBuffers: number) {\n this.opentui.symbols.updateMemoryStats(renderer, heapUsed, heapTotal, arrayBuffers)\n }\n\n public getNextBuffer(renderer: Pointer): OptimizedBuffer {\n const bufferPtr = this.opentui.symbols.getNextBuffer(renderer)\n if (!bufferPtr) {\n throw new Error(\"Failed to get next buffer\")\n }\n\n const width = this.opentui.symbols.getBufferWidth(bufferPtr)\n const height = this.opentui.symbols.getBufferHeight(bufferPtr)\n\n return new OptimizedBuffer(this, bufferPtr, width, height, { id: \"next buffer\", widthMethod: \"unicode\" })\n }\n\n public getCurrentBuffer(renderer: Pointer): OptimizedBuffer {\n const bufferPtr = this.opentui.symbols.getCurrentBuffer(renderer)\n if (!bufferPtr) {\n throw new Error(\"Failed to get current buffer\")\n }\n\n const width = this.opentui.symbols.getBufferWidth(bufferPtr)\n const height = this.opentui.symbols.getBufferHeight(bufferPtr)\n\n return new OptimizedBuffer(this, bufferPtr, width, height, { id: \"current buffer\", widthMethod: \"unicode\" })\n }\n\n public bufferGetCharPtr(buffer: Pointer): Pointer {\n const ptr = this.opentui.symbols.bufferGetCharPtr(buffer)\n if (!ptr) {\n throw new Error(\"Failed to get char pointer\")\n }\n return ptr\n }\n\n public bufferGetFgPtr(buffer: Pointer): Pointer {\n const ptr = this.opentui.symbols.bufferGetFgPtr(buffer)\n if (!ptr) {\n throw new Error(\"Failed to get fg pointer\")\n }\n return ptr\n }\n\n public bufferGetBgPtr(buffer: Pointer): Pointer {\n const ptr = this.opentui.symbols.bufferGetBgPtr(buffer)\n if (!ptr) {\n throw new Error(\"Failed to get bg pointer\")\n }\n return ptr\n }\n\n public bufferGetAttributesPtr(buffer: Pointer): Pointer {\n const ptr = this.opentui.symbols.bufferGetAttributesPtr(buffer)\n if (!ptr) {\n throw new Error(\"Failed to get attributes pointer\")\n }\n return ptr\n }\n\n public bufferGetRespectAlpha(buffer: Pointer): boolean {\n return this.opentui.symbols.bufferGetRespectAlpha(buffer)\n }\n\n public bufferSetRespectAlpha(buffer: Pointer, respectAlpha: boolean): void {\n this.opentui.symbols.bufferSetRespectAlpha(buffer, respectAlpha)\n }\n\n public bufferGetId(buffer: Pointer): string {\n const maxLen = 256\n const outBuffer = new Uint8Array(maxLen)\n const actualLen = this.opentui.symbols.bufferGetId(buffer, outBuffer, maxLen)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n return this.decoder.decode(outBuffer.slice(0, len))\n }\n\n public bufferGetRealCharSize(buffer: Pointer): number {\n return this.opentui.symbols.bufferGetRealCharSize(buffer)\n }\n\n public bufferWriteResolvedChars(buffer: Pointer, outputBuffer: Uint8Array, addLineBreaks: boolean): number {\n const bytesWritten = this.opentui.symbols.bufferWriteResolvedChars(\n buffer,\n outputBuffer,\n outputBuffer.length,\n addLineBreaks,\n )\n return typeof bytesWritten === \"bigint\" ? Number(bytesWritten) : bytesWritten\n }\n\n public getBufferWidth(buffer: Pointer): number {\n return this.opentui.symbols.getBufferWidth(buffer)\n }\n\n public getBufferHeight(buffer: Pointer): number {\n return this.opentui.symbols.getBufferHeight(buffer)\n }\n\n public bufferClear(buffer: Pointer, color: RGBA) {\n this.opentui.symbols.bufferClear(buffer, color.buffer)\n }\n\n public bufferDrawText(\n buffer: Pointer,\n text: string,\n x: number,\n y: number,\n color: RGBA,\n bgColor?: RGBA,\n attributes?: number,\n ) {\n const textBytes = this.encoder.encode(text)\n const textLength = textBytes.byteLength\n const bg = bgColor ? bgColor.buffer : null\n const fg = color.buffer\n\n this.opentui.symbols.bufferDrawText(buffer, textBytes, textLength, x, y, fg, bg, attributes ?? 0)\n }\n\n public bufferSetCellWithAlphaBlending(\n buffer: Pointer,\n x: number,\n y: number,\n char: string,\n color: RGBA,\n bgColor: RGBA,\n attributes?: number,\n ) {\n const charPtr = char.codePointAt(0) ?? \" \".codePointAt(0)!\n const bg = bgColor.buffer\n const fg = color.buffer\n\n this.opentui.symbols.bufferSetCellWithAlphaBlending(buffer, x, y, charPtr, fg, bg, attributes ?? 0)\n }\n\n public bufferSetCell(\n buffer: Pointer,\n x: number,\n y: number,\n char: string,\n color: RGBA,\n bgColor: RGBA,\n attributes?: number,\n ) {\n const charPtr = char.codePointAt(0) ?? \" \".codePointAt(0)!\n const bg = bgColor.buffer\n const fg = color.buffer\n\n this.opentui.symbols.bufferSetCell(buffer, x, y, charPtr, fg, bg, attributes ?? 0)\n }\n\n public bufferFillRect(buffer: Pointer, x: number, y: number, width: number, height: number, color: RGBA) {\n const bg = color.buffer\n this.opentui.symbols.bufferFillRect(buffer, x, y, width, height, bg)\n }\n\n public bufferColorMatrix(\n buffer: Pointer,\n matrixPtr: Pointer,\n cellMaskPtr: Pointer,\n cellMaskCount: number,\n strength: number,\n target: TargetChannel,\n ): void {\n this.opentui.symbols.bufferColorMatrix(buffer, matrixPtr, cellMaskPtr, cellMaskCount, strength, target)\n }\n\n public bufferColorMatrixUniform(buffer: Pointer, matrixPtr: Pointer, strength: number, target: TargetChannel): void {\n this.opentui.symbols.bufferColorMatrixUniform(buffer, matrixPtr, strength, target)\n }\n\n public bufferDrawSuperSampleBuffer(\n buffer: Pointer,\n x: number,\n y: number,\n pixelDataPtr: Pointer,\n pixelDataLength: number,\n format: \"bgra8unorm\" | \"rgba8unorm\",\n alignedBytesPerRow: number,\n ): void {\n const formatId = format === \"bgra8unorm\" ? 0 : 1\n this.opentui.symbols.bufferDrawSuperSampleBuffer(\n buffer,\n x,\n y,\n pixelDataPtr,\n pixelDataLength,\n formatId,\n alignedBytesPerRow,\n )\n }\n\n public bufferDrawPackedBuffer(\n buffer: Pointer,\n dataPtr: Pointer,\n dataLen: number,\n posX: number,\n posY: number,\n terminalWidthCells: number,\n terminalHeightCells: number,\n ): void {\n this.opentui.symbols.bufferDrawPackedBuffer(\n buffer,\n dataPtr,\n dataLen,\n posX,\n posY,\n terminalWidthCells,\n terminalHeightCells,\n )\n }\n\n public bufferDrawGrayscaleBuffer(\n buffer: Pointer,\n posX: number,\n posY: number,\n intensitiesPtr: Pointer,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null,\n bg: RGBA | null,\n ): void {\n this.opentui.symbols.bufferDrawGrayscaleBuffer(\n buffer,\n posX,\n posY,\n intensitiesPtr,\n srcWidth,\n srcHeight,\n fg?.buffer ?? null,\n bg?.buffer ?? null,\n )\n }\n\n public bufferDrawGrayscaleBufferSupersampled(\n buffer: Pointer,\n posX: number,\n posY: number,\n intensitiesPtr: Pointer,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null,\n bg: RGBA | null,\n ): void {\n this.opentui.symbols.bufferDrawGrayscaleBufferSupersampled(\n buffer,\n posX,\n posY,\n intensitiesPtr,\n srcWidth,\n srcHeight,\n fg?.buffer ?? null,\n bg?.buffer ?? null,\n )\n }\n\n public bufferDrawGrid(\n buffer: Pointer,\n borderChars: Uint32Array,\n borderFg: RGBA,\n borderBg: RGBA,\n columnOffsets: Int32Array,\n columnCount: number,\n rowOffsets: Int32Array,\n rowCount: number,\n options: { drawInner: boolean; drawOuter: boolean },\n ): void {\n const optionsBuffer = GridDrawOptionsStruct.pack({\n drawInner: options.drawInner,\n drawOuter: options.drawOuter,\n })\n\n this.opentui.symbols.bufferDrawGrid(\n buffer,\n borderChars,\n borderFg.buffer,\n borderBg.buffer,\n columnOffsets,\n columnCount,\n rowOffsets,\n rowCount,\n ptr(optionsBuffer),\n )\n }\n\n public bufferDrawBox(\n buffer: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n borderChars: Uint32Array,\n packedOptions: number,\n borderColor: RGBA,\n backgroundColor: RGBA,\n title: string | null,\n ): void {\n const titleBytes = title ? this.encoder.encode(title) : null\n const titleLen = title ? titleBytes!.length : 0\n const titlePtr = title ? titleBytes : null\n\n this.opentui.symbols.bufferDrawBox(\n buffer,\n x,\n y,\n width,\n height,\n borderChars,\n packedOptions,\n borderColor.buffer,\n backgroundColor.buffer,\n titlePtr,\n titleLen,\n )\n }\n\n public bufferResize(buffer: Pointer, width: number, height: number): void {\n this.opentui.symbols.bufferResize(buffer, width, height)\n }\n\n // Link API\n public linkAlloc(url: string): number {\n const urlBytes = this.encoder.encode(url)\n return this.opentui.symbols.linkAlloc(urlBytes, urlBytes.length)\n }\n\n public linkGetUrl(linkId: number, maxLen: number = 512): string {\n const outBuffer = new Uint8Array(maxLen)\n const actualLen = this.opentui.symbols.linkGetUrl(linkId, outBuffer, maxLen)\n return this.decoder.decode(outBuffer.slice(0, actualLen))\n }\n\n public attributesWithLink(baseAttributes: number, linkId: number): number {\n return this.opentui.symbols.attributesWithLink(baseAttributes, linkId)\n }\n\n public attributesGetLinkId(attributes: number): number {\n return this.opentui.symbols.attributesGetLinkId(attributes)\n }\n\n public resizeRenderer(renderer: Pointer, width: number, height: number) {\n this.opentui.symbols.resizeRenderer(renderer, width, height)\n }\n\n public setCursorPosition(renderer: Pointer, x: number, y: number, visible: boolean) {\n this.opentui.symbols.setCursorPosition(renderer, x, y, visible)\n }\n\n public setCursorColor(renderer: Pointer, color: RGBA) {\n this.opentui.symbols.setCursorColor(renderer, color.buffer)\n }\n\n public getCursorState(renderer: Pointer): CursorState {\n const cursorBuffer = new ArrayBuffer(CursorStateStruct.size)\n this.opentui.symbols.getCursorState(renderer, ptr(cursorBuffer))\n const struct = CursorStateStruct.unpack(cursorBuffer)\n\n return {\n x: struct.x,\n y: struct.y,\n visible: struct.visible,\n style: CURSOR_ID_TO_STYLE[struct.style] ?? \"block\",\n blinking: struct.blinking,\n color: RGBA.fromValues(struct.r, struct.g, struct.b, struct.a),\n }\n }\n\n public setCursorStyleOptions(renderer: Pointer, options: CursorStyleOptions): void {\n const style = options.style != null ? CURSOR_STYLE_TO_ID[options.style] : 255\n const blinking = options.blinking != null ? (options.blinking ? 1 : 0) : 255\n const cursor = options.cursor != null ? MOUSE_STYLE_TO_ID[options.cursor] : 255\n\n const buffer = CursorStyleOptionsStruct.pack({ style, blinking, color: options.color, cursor })\n this.opentui.symbols.setCursorStyleOptions(renderer, ptr(buffer))\n }\n\n public render(renderer: Pointer, force: boolean) {\n this.opentui.symbols.render(renderer, force)\n }\n\n public createOptimizedBuffer(\n width: number,\n height: number,\n widthMethod: WidthMethod,\n respectAlpha: boolean = false,\n id?: string,\n ): OptimizedBuffer {\n if (Number.isNaN(width) || Number.isNaN(height)) {\n console.error(new Error(`Invalid dimensions for OptimizedBuffer: ${width}x${height}`).stack)\n }\n\n const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n const idToUse = id || \"unnamed buffer\"\n const idBytes = this.encoder.encode(idToUse)\n const bufferPtr = this.opentui.symbols.createOptimizedBuffer(\n width,\n height,\n respectAlpha,\n widthMethodCode,\n idBytes,\n idBytes.length,\n )\n if (!bufferPtr) {\n throw new Error(`Failed to create optimized buffer: ${width}x${height}`)\n }\n\n return new OptimizedBuffer(this, bufferPtr, width, height, { respectAlpha, id, widthMethod })\n }\n\n public destroyOptimizedBuffer(bufferPtr: Pointer) {\n this.opentui.symbols.destroyOptimizedBuffer(bufferPtr)\n }\n\n public drawFrameBuffer(\n targetBufferPtr: Pointer,\n destX: number,\n destY: number,\n bufferPtr: Pointer,\n sourceX?: number,\n sourceY?: number,\n sourceWidth?: number,\n sourceHeight?: number,\n ) {\n const srcX = sourceX ?? 0\n const srcY = sourceY ?? 0\n const srcWidth = sourceWidth ?? 0\n const srcHeight = sourceHeight ?? 0\n this.opentui.symbols.drawFrameBuffer(targetBufferPtr, destX, destY, bufferPtr, srcX, srcY, srcWidth, srcHeight)\n }\n\n public setDebugOverlay(renderer: Pointer, enabled: boolean, corner: DebugOverlayCorner) {\n this.opentui.symbols.setDebugOverlay(renderer, enabled, corner)\n }\n\n public clearTerminal(renderer: Pointer) {\n this.opentui.symbols.clearTerminal(renderer)\n }\n\n public setTerminalTitle(renderer: Pointer, title: string) {\n const titleBytes = this.encoder.encode(title)\n this.opentui.symbols.setTerminalTitle(renderer, titleBytes, titleBytes.length)\n }\n\n public copyToClipboardOSC52(renderer: Pointer, target: number, payload: Uint8Array): boolean {\n return this.opentui.symbols.copyToClipboardOSC52(renderer, target, payload, payload.length)\n }\n\n public clearClipboardOSC52(renderer: Pointer, target: number): boolean {\n return this.opentui.symbols.clearClipboardOSC52(renderer, target)\n }\n\n public addToHitGrid(renderer: Pointer, x: number, y: number, width: number, height: number, id: number) {\n this.opentui.symbols.addToHitGrid(renderer, x, y, width, height, id)\n }\n\n public clearCurrentHitGrid(renderer: Pointer) {\n this.opentui.symbols.clearCurrentHitGrid(renderer)\n }\n\n public hitGridPushScissorRect(renderer: Pointer, x: number, y: number, width: number, height: number) {\n this.opentui.symbols.hitGridPushScissorRect(renderer, x, y, width, height)\n }\n\n public hitGridPopScissorRect(renderer: Pointer) {\n this.opentui.symbols.hitGridPopScissorRect(renderer)\n }\n\n public hitGridClearScissorRects(renderer: Pointer) {\n this.opentui.symbols.hitGridClearScissorRects(renderer)\n }\n\n public addToCurrentHitGridClipped(\n renderer: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n id: number,\n ) {\n this.opentui.symbols.addToCurrentHitGridClipped(renderer, x, y, width, height, id)\n }\n\n public checkHit(renderer: Pointer, x: number, y: number): number {\n return this.opentui.symbols.checkHit(renderer, x, y)\n }\n\n public getHitGridDirty(renderer: Pointer): boolean {\n return this.opentui.symbols.getHitGridDirty(renderer)\n }\n\n public dumpHitGrid(renderer: Pointer): void {\n this.opentui.symbols.dumpHitGrid(renderer)\n }\n\n public dumpBuffers(renderer: Pointer, timestamp?: number): void {\n const ts = timestamp ?? Date.now()\n this.opentui.symbols.dumpBuffers(renderer, ts)\n }\n\n public dumpStdoutBuffer(renderer: Pointer, timestamp?: number): void {\n const ts = timestamp ?? Date.now()\n this.opentui.symbols.dumpStdoutBuffer(renderer, ts)\n }\n\n public restoreTerminalModes(renderer: Pointer): void {\n this.opentui.symbols.restoreTerminalModes(renderer)\n }\n\n public enableMouse(renderer: Pointer, enableMovement: boolean): void {\n this.opentui.symbols.enableMouse(renderer, enableMovement)\n }\n\n public disableMouse(renderer: Pointer): void {\n this.opentui.symbols.disableMouse(renderer)\n }\n\n public enableKittyKeyboard(renderer: Pointer, flags: number): void {\n this.opentui.symbols.enableKittyKeyboard(renderer, flags)\n }\n\n public disableKittyKeyboard(renderer: Pointer): void {\n this.opentui.symbols.disableKittyKeyboard(renderer)\n }\n\n public setKittyKeyboardFlags(renderer: Pointer, flags: number): void {\n this.opentui.symbols.setKittyKeyboardFlags(renderer, flags)\n }\n\n public getKittyKeyboardFlags(renderer: Pointer): number {\n return this.opentui.symbols.getKittyKeyboardFlags(renderer)\n }\n\n public setupTerminal(renderer: Pointer, useAlternateScreen: boolean): void {\n this.opentui.symbols.setupTerminal(renderer, useAlternateScreen)\n }\n\n public suspendRenderer(renderer: Pointer): void {\n this.opentui.symbols.suspendRenderer(renderer)\n }\n\n public resumeRenderer(renderer: Pointer): void {\n this.opentui.symbols.resumeRenderer(renderer)\n }\n\n public queryPixelResolution(renderer: Pointer): void {\n this.opentui.symbols.queryPixelResolution(renderer)\n }\n\n /**\n * Write data to stdout, synchronizing with the render thread if necessary.\n * This should be used for ALL stdout writes to avoid race conditions when\n * the render thread is active.\n */\n public writeOut(renderer: Pointer, data: string | Uint8Array): void {\n const bytes = typeof data === \"string\" ? new TextEncoder().encode(data) : data\n if (bytes.length === 0) return\n this.opentui.symbols.writeOut(renderer, ptr(bytes), bytes.length)\n }\n\n // TextBuffer methods\n public createTextBuffer(widthMethod: WidthMethod): TextBuffer {\n const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n const bufferPtr = this.opentui.symbols.createTextBuffer(widthMethodCode)\n if (!bufferPtr) {\n throw new Error(`Failed to create TextBuffer`)\n }\n\n return new TextBuffer(this, bufferPtr)\n }\n\n public destroyTextBuffer(buffer: Pointer): void {\n this.opentui.symbols.destroyTextBuffer(buffer)\n }\n\n public textBufferGetLength(buffer: Pointer): number {\n return this.opentui.symbols.textBufferGetLength(buffer)\n }\n\n public textBufferGetByteSize(buffer: Pointer): number {\n return this.opentui.symbols.textBufferGetByteSize(buffer)\n }\n\n public textBufferReset(buffer: Pointer): void {\n this.opentui.symbols.textBufferReset(buffer)\n }\n\n public textBufferClear(buffer: Pointer): void {\n this.opentui.symbols.textBufferClear(buffer)\n }\n\n public textBufferSetDefaultFg(buffer: Pointer, fg: RGBA | null): void {\n const fgPtr = fg ? fg.buffer : null\n this.opentui.symbols.textBufferSetDefaultFg(buffer, fgPtr)\n }\n\n public textBufferSetDefaultBg(buffer: Pointer, bg: RGBA | null): void {\n const bgPtr = bg ? bg.buffer : null\n this.opentui.symbols.textBufferSetDefaultBg(buffer, bgPtr)\n }\n\n public textBufferSetDefaultAttributes(buffer: Pointer, attributes: number | null): void {\n const attrValue = attributes === null ? null : new Uint8Array([attributes])\n this.opentui.symbols.textBufferSetDefaultAttributes(buffer, attrValue)\n }\n\n public textBufferResetDefaults(buffer: Pointer): void {\n this.opentui.symbols.textBufferResetDefaults(buffer)\n }\n\n public textBufferGetTabWidth(buffer: Pointer): number {\n return this.opentui.symbols.textBufferGetTabWidth(buffer)\n }\n\n public textBufferSetTabWidth(buffer: Pointer, width: number): void {\n this.opentui.symbols.textBufferSetTabWidth(buffer, width)\n }\n\n public textBufferRegisterMemBuffer(buffer: Pointer, bytes: Uint8Array, owned: boolean = false): number {\n const result = this.opentui.symbols.textBufferRegisterMemBuffer(buffer, bytes, bytes.length, owned)\n if (result === 0xffff) {\n throw new Error(\"Failed to register memory buffer\")\n }\n return result\n }\n\n public textBufferReplaceMemBuffer(\n buffer: Pointer,\n memId: number,\n bytes: Uint8Array,\n owned: boolean = false,\n ): boolean {\n return this.opentui.symbols.textBufferReplaceMemBuffer(buffer, memId, bytes, bytes.length, owned)\n }\n\n public textBufferClearMemRegistry(buffer: Pointer): void {\n this.opentui.symbols.textBufferClearMemRegistry(buffer)\n }\n\n public textBufferSetTextFromMem(buffer: Pointer, memId: number): void {\n this.opentui.symbols.textBufferSetTextFromMem(buffer, memId)\n }\n\n public textBufferAppend(buffer: Pointer, bytes: Uint8Array): void {\n this.opentui.symbols.textBufferAppend(buffer, bytes, bytes.length)\n }\n\n public textBufferAppendFromMemId(buffer: Pointer, memId: number): void {\n this.opentui.symbols.textBufferAppendFromMemId(buffer, memId)\n }\n\n public textBufferLoadFile(buffer: Pointer, path: string): boolean {\n const pathBytes = this.encoder.encode(path)\n return this.opentui.symbols.textBufferLoadFile(buffer, pathBytes, pathBytes.length)\n }\n\n public textBufferSetStyledText(\n buffer: Pointer,\n chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number; link?: { url: string } }>,\n ): void {\n if (chunks.length === 0) {\n this.textBufferClear(buffer)\n return\n }\n\n const chunksBuffer = StyledChunkStruct.packList(chunks)\n this.opentui.symbols.textBufferSetStyledText(buffer, ptr(chunksBuffer), chunks.length)\n }\n\n public textBufferGetLineCount(buffer: Pointer): number {\n return this.opentui.symbols.textBufferGetLineCount(buffer)\n }\n\n private textBufferGetPlainText(buffer: Pointer, outPtr: Pointer, maxLen: number): number {\n const result = this.opentui.symbols.textBufferGetPlainText(buffer, outPtr, maxLen)\n return typeof result === \"bigint\" ? Number(result) : result\n }\n\n public getPlainTextBytes(buffer: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n\n const actualLen = this.textBufferGetPlainText(buffer, ptr(outBuffer), maxLength)\n\n if (actualLen === 0) {\n return null\n }\n\n return outBuffer.slice(0, actualLen)\n }\n\n public textBufferGetTextRange(\n buffer: Pointer,\n startOffset: number,\n endOffset: number,\n maxLength: number,\n ): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n\n const actualLen = this.opentui.symbols.textBufferGetTextRange(\n buffer,\n startOffset,\n endOffset,\n ptr(outBuffer),\n maxLength,\n )\n\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n\n if (len === 0) {\n return null\n }\n\n return outBuffer.slice(0, len)\n }\n\n public textBufferGetTextRangeByCoords(\n buffer: Pointer,\n startRow: number,\n startCol: number,\n endRow: number,\n endCol: number,\n maxLength: number,\n ): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n\n const actualLen = this.opentui.symbols.textBufferGetTextRangeByCoords(\n buffer,\n startRow,\n startCol,\n endRow,\n endCol,\n ptr(outBuffer),\n maxLength,\n )\n\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n\n if (len === 0) {\n return null\n }\n\n return outBuffer.slice(0, len)\n }\n\n // TextBufferView methods\n public createTextBufferView(textBuffer: Pointer): Pointer {\n const viewPtr = this.opentui.symbols.createTextBufferView(textBuffer)\n if (!viewPtr) {\n throw new Error(\"Failed to create TextBufferView\")\n }\n return viewPtr\n }\n\n public destroyTextBufferView(view: Pointer): void {\n this.opentui.symbols.destroyTextBufferView(view)\n }\n\n public textBufferViewSetSelection(\n view: Pointer,\n start: number,\n end: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ): void {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n this.opentui.symbols.textBufferViewSetSelection(view, start, end, bg, fg)\n }\n\n public textBufferViewResetSelection(view: Pointer): void {\n this.opentui.symbols.textBufferViewResetSelection(view)\n }\n\n public textBufferViewGetSelection(view: Pointer): { start: number; end: number } | null {\n const packedInfo = this.textBufferViewGetSelectionInfo(view)\n\n // Check for no selection marker (0xFFFFFFFF_FFFFFFFF)\n if (packedInfo === 0xffff_ffff_ffff_ffffn) {\n return null\n }\n\n const start = Number(packedInfo >> 32n)\n const end = Number(packedInfo & 0xffff_ffffn)\n\n return { start, end }\n }\n\n private textBufferViewGetSelectionInfo(view: Pointer): bigint {\n return this.opentui.symbols.textBufferViewGetSelectionInfo(view)\n }\n\n public textBufferViewSetLocalSelection(\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ): boolean {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n return this.opentui.symbols.textBufferViewSetLocalSelection(view, anchorX, anchorY, focusX, focusY, bg, fg)\n }\n\n public textBufferViewUpdateSelection(view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null): void {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n this.opentui.symbols.textBufferViewUpdateSelection(view, end, bg, fg)\n }\n\n public textBufferViewUpdateLocalSelection(\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ): boolean {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n return this.opentui.symbols.textBufferViewUpdateLocalSelection(view, anchorX, anchorY, focusX, focusY, bg, fg)\n }\n\n public textBufferViewResetLocalSelection(view: Pointer): void {\n this.opentui.symbols.textBufferViewResetLocalSelection(view)\n }\n\n public textBufferViewSetWrapWidth(view: Pointer, width: number): void {\n this.opentui.symbols.textBufferViewSetWrapWidth(view, width)\n }\n\n public textBufferViewSetWrapMode(view: Pointer, mode: \"none\" | \"char\" | \"word\"): void {\n const modeValue = mode === \"none\" ? 0 : mode === \"char\" ? 1 : 2\n this.opentui.symbols.textBufferViewSetWrapMode(view, modeValue)\n }\n\n public textBufferViewSetViewportSize(view: Pointer, width: number, height: number): void {\n this.opentui.symbols.textBufferViewSetViewportSize(view, width, height)\n }\n\n public textBufferViewSetViewport(view: Pointer, x: number, y: number, width: number, height: number): void {\n this.opentui.symbols.textBufferViewSetViewport(view, x, y, width, height)\n }\n\n public textBufferViewGetLineInfo(view: Pointer): LineInfo {\n const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n this.textBufferViewGetLineInfoDirect(view, ptr(outBuffer))\n const struct = LineInfoStruct.unpack(outBuffer)\n\n const lineStartCols = struct.startCols as number[]\n const lineWidthCols = struct.widthCols as number[]\n const lineWidthColsMax = struct.widthColsMax\n\n return {\n lineStartCols,\n lineWidthCols,\n lineWidthColsMax,\n lineSources: struct.sources as number[],\n lineWraps: struct.wraps as number[],\n }\n }\n\n public textBufferViewGetLogicalLineInfo(view: Pointer): LineInfo {\n const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n this.textBufferViewGetLogicalLineInfoDirect(view, ptr(outBuffer))\n const struct = LineInfoStruct.unpack(outBuffer)\n\n const lineStartCols = struct.startCols as number[]\n const lineWidthCols = struct.widthCols as number[]\n const lineWidthColsMax = struct.widthColsMax\n\n return {\n lineStartCols,\n lineWidthCols,\n lineWidthColsMax,\n lineSources: struct.sources as number[],\n lineWraps: struct.wraps as number[],\n }\n }\n\n public textBufferViewGetVirtualLineCount(view: Pointer): number {\n return this.opentui.symbols.textBufferViewGetVirtualLineCount(view)\n }\n\n private textBufferViewGetLineInfoDirect(view: Pointer, outPtr: Pointer): void {\n this.opentui.symbols.textBufferViewGetLineInfoDirect(view, outPtr)\n }\n\n private textBufferViewGetLogicalLineInfoDirect(view: Pointer, outPtr: Pointer): void {\n this.opentui.symbols.textBufferViewGetLogicalLineInfoDirect(view, outPtr)\n }\n\n private textBufferViewGetSelectedText(view: Pointer, outPtr: Pointer, maxLen: number): number {\n const result = this.opentui.symbols.textBufferViewGetSelectedText(view, outPtr, maxLen)\n return typeof result === \"bigint\" ? Number(result) : result\n }\n\n private textBufferViewGetPlainText(view: Pointer, outPtr: Pointer, maxLen: number): number {\n const result = this.opentui.symbols.textBufferViewGetPlainText(view, outPtr, maxLen)\n return typeof result === \"bigint\" ? Number(result) : result\n }\n\n public textBufferViewGetSelectedTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n\n const actualLen = this.textBufferViewGetSelectedText(view, ptr(outBuffer), maxLength)\n\n if (actualLen === 0) {\n return null\n }\n\n return outBuffer.slice(0, actualLen)\n }\n\n public textBufferViewGetPlainTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n\n const actualLen = this.textBufferViewGetPlainText(view, ptr(outBuffer), maxLength)\n\n if (actualLen === 0) {\n return null\n }\n\n return outBuffer.slice(0, actualLen)\n }\n\n public textBufferViewSetTabIndicator(view: Pointer, indicator: number): void {\n this.opentui.symbols.textBufferViewSetTabIndicator(view, indicator)\n }\n\n public textBufferViewSetTabIndicatorColor(view: Pointer, color: RGBA): void {\n this.opentui.symbols.textBufferViewSetTabIndicatorColor(view, color.buffer)\n }\n\n public textBufferViewSetTruncate(view: Pointer, truncate: boolean): void {\n this.opentui.symbols.textBufferViewSetTruncate(view, truncate)\n }\n\n public textBufferViewMeasureForDimensions(\n view: Pointer,\n width: number,\n height: number,\n ): { lineCount: number; widthColsMax: number } | null {\n const resultBuffer = new ArrayBuffer(MeasureResultStruct.size)\n const resultPtr = ptr(new Uint8Array(resultBuffer))\n const success = this.opentui.symbols.textBufferViewMeasureForDimensions(view, width, height, resultPtr)\n if (!success) {\n return null\n }\n const result = MeasureResultStruct.unpack(resultBuffer)\n return result\n }\n\n public textBufferAddHighlightByCharRange(buffer: Pointer, highlight: Highlight): void {\n const packedHighlight = HighlightStruct.pack(highlight)\n this.opentui.symbols.textBufferAddHighlightByCharRange(buffer, ptr(packedHighlight))\n }\n\n public textBufferAddHighlight(buffer: Pointer, lineIdx: number, highlight: Highlight): void {\n const packedHighlight = HighlightStruct.pack(highlight)\n this.opentui.symbols.textBufferAddHighlight(buffer, lineIdx, ptr(packedHighlight))\n }\n\n public textBufferRemoveHighlightsByRef(buffer: Pointer, hlRef: number): void {\n this.opentui.symbols.textBufferRemoveHighlightsByRef(buffer, hlRef)\n }\n\n public textBufferClearLineHighlights(buffer: Pointer, lineIdx: number): void {\n this.opentui.symbols.textBufferClearLineHighlights(buffer, lineIdx)\n }\n\n public textBufferClearAllHighlights(buffer: Pointer): void {\n this.opentui.symbols.textBufferClearAllHighlights(buffer)\n }\n\n public textBufferSetSyntaxStyle(buffer: Pointer, style: Pointer | null): void {\n this.opentui.symbols.textBufferSetSyntaxStyle(buffer, style)\n }\n\n public textBufferGetLineHighlights(buffer: Pointer, lineIdx: number): Array<Highlight> {\n const outCountBuf = new BigUint64Array(1)\n\n const nativePtr = this.opentui.symbols.textBufferGetLineHighlightsPtr(buffer, lineIdx, ptr(outCountBuf))\n if (!nativePtr) return []\n\n const count = Number(outCountBuf[0])\n const byteLen = count * HighlightStruct.size\n const raw = toArrayBuffer(nativePtr, 0, byteLen)\n const results = HighlightStruct.unpackList(raw, count)\n\n this.opentui.symbols.textBufferFreeLineHighlights(nativePtr, count)\n\n return results\n }\n\n public textBufferGetHighlightCount(buffer: Pointer): number {\n return this.opentui.symbols.textBufferGetHighlightCount(buffer)\n }\n\n public getArenaAllocatedBytes(): number {\n const result = this.opentui.symbols.getArenaAllocatedBytes()\n return typeof result === \"bigint\" ? Number(result) : result\n }\n\n public getBuildOptions(): BuildOptions {\n const optionsBuffer = new ArrayBuffer(BuildOptionsStruct.size)\n this.opentui.symbols.getBuildOptions(ptr(optionsBuffer))\n const options = BuildOptionsStruct.unpack(optionsBuffer)\n\n return {\n gpaSafeStats: !!options.gpaSafeStats,\n gpaMemoryLimitTracking: !!options.gpaMemoryLimitTracking,\n }\n }\n\n public getAllocatorStats(): AllocatorStats {\n const statsBuffer = new ArrayBuffer(AllocatorStatsStruct.size)\n this.opentui.symbols.getAllocatorStats(ptr(statsBuffer))\n const stats = AllocatorStatsStruct.unpack(statsBuffer)\n\n return {\n totalRequestedBytes: toNumber(stats.totalRequestedBytes),\n activeAllocations: toNumber(stats.activeAllocations),\n smallAllocations: toNumber(stats.smallAllocations),\n largeAllocations: toNumber(stats.largeAllocations),\n requestedBytesValid: !!stats.requestedBytesValid,\n }\n }\n\n public bufferDrawTextBufferView(buffer: Pointer, view: Pointer, x: number, y: number): void {\n this.opentui.symbols.bufferDrawTextBufferView(buffer, view, x, y)\n }\n\n public bufferDrawEditorView(buffer: Pointer, view: Pointer, x: number, y: number): void {\n this.opentui.symbols.bufferDrawEditorView(buffer, view, x, y)\n }\n\n // EditorView methods\n public createEditorView(editBufferPtr: Pointer, viewportWidth: number, viewportHeight: number): Pointer {\n const viewPtr = this.opentui.symbols.createEditorView(editBufferPtr, viewportWidth, viewportHeight)\n if (!viewPtr) {\n throw new Error(\"Failed to create EditorView\")\n }\n return viewPtr\n }\n\n public destroyEditorView(view: Pointer): void {\n this.opentui.symbols.destroyEditorView(view)\n }\n\n public editorViewSetViewportSize(view: Pointer, width: number, height: number): void {\n this.opentui.symbols.editorViewSetViewportSize(view, width, height)\n }\n\n public editorViewSetViewport(\n view: Pointer,\n x: number,\n y: number,\n width: number,\n height: number,\n moveCursor: boolean,\n ): void {\n this.opentui.symbols.editorViewSetViewport(view, x, y, width, height, moveCursor)\n }\n\n public editorViewGetViewport(view: Pointer): { offsetY: number; offsetX: number; height: number; width: number } {\n const x = new Uint32Array(1)\n const y = new Uint32Array(1)\n const width = new Uint32Array(1)\n const height = new Uint32Array(1)\n\n this.opentui.symbols.editorViewGetViewport(view, ptr(x), ptr(y), ptr(width), ptr(height))\n\n return {\n offsetX: x[0],\n offsetY: y[0],\n width: width[0],\n height: height[0],\n }\n }\n\n public editorViewSetScrollMargin(view: Pointer, margin: number): void {\n this.opentui.symbols.editorViewSetScrollMargin(view, margin)\n }\n\n public editorViewSetWrapMode(view: Pointer, mode: \"none\" | \"char\" | \"word\"): void {\n const modeValue = mode === \"none\" ? 0 : mode === \"char\" ? 1 : 2\n this.opentui.symbols.editorViewSetWrapMode(view, modeValue)\n }\n\n public editorViewGetVirtualLineCount(view: Pointer): number {\n return this.opentui.symbols.editorViewGetVirtualLineCount(view)\n }\n\n public editorViewGetTotalVirtualLineCount(view: Pointer): number {\n return this.opentui.symbols.editorViewGetTotalVirtualLineCount(view)\n }\n\n public editorViewGetTextBufferView(view: Pointer): Pointer {\n const result = this.opentui.symbols.editorViewGetTextBufferView(view)\n if (!result) {\n throw new Error(\"Failed to get TextBufferView from EditorView\")\n }\n return result\n }\n\n public editorViewGetLineInfo(view: Pointer): LineInfo {\n const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n this.opentui.symbols.editorViewGetLineInfoDirect(view, ptr(outBuffer))\n const struct = LineInfoStruct.unpack(outBuffer)\n\n const lineStartCols = struct.startCols as number[]\n const lineWidthCols = struct.widthCols as number[]\n const lineWidthColsMax = struct.widthColsMax\n\n return {\n lineStartCols,\n lineWidthCols,\n lineWidthColsMax,\n lineSources: struct.sources as number[],\n lineWraps: struct.wraps as number[],\n }\n }\n\n public editorViewGetLogicalLineInfo(view: Pointer): LineInfo {\n const outBuffer = new ArrayBuffer(LineInfoStruct.size)\n this.opentui.symbols.editorViewGetLogicalLineInfoDirect(view, ptr(outBuffer))\n const struct = LineInfoStruct.unpack(outBuffer)\n\n const lineStartCols = struct.startCols as number[]\n const lineWidthCols = struct.widthCols as number[]\n const lineWidthColsMax = struct.widthColsMax\n\n return {\n lineStartCols,\n lineWidthCols,\n lineWidthColsMax,\n lineSources: struct.sources as number[],\n lineWraps: struct.wraps as number[],\n }\n }\n\n // EditBuffer implementations\n public createEditBuffer(widthMethod: WidthMethod): Pointer {\n const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n const bufferPtr = this.opentui.symbols.createEditBuffer(widthMethodCode)\n if (!bufferPtr) {\n throw new Error(\"Failed to create EditBuffer\")\n }\n return bufferPtr\n }\n\n public destroyEditBuffer(buffer: Pointer): void {\n this.opentui.symbols.destroyEditBuffer(buffer)\n }\n\n public editBufferSetText(buffer: Pointer, textBytes: Uint8Array): void {\n this.opentui.symbols.editBufferSetText(buffer, textBytes, textBytes.length)\n }\n\n public editBufferSetTextFromMem(buffer: Pointer, memId: number): void {\n this.opentui.symbols.editBufferSetTextFromMem(buffer, memId)\n }\n\n public editBufferReplaceText(buffer: Pointer, textBytes: Uint8Array): void {\n this.opentui.symbols.editBufferReplaceText(buffer, textBytes, textBytes.length)\n }\n\n public editBufferReplaceTextFromMem(buffer: Pointer, memId: number): void {\n this.opentui.symbols.editBufferReplaceTextFromMem(buffer, memId)\n }\n\n public editBufferGetText(buffer: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editBufferGetText(buffer, ptr(outBuffer), maxLength)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editBufferInsertChar(buffer: Pointer, char: string): void {\n const charBytes = this.encoder.encode(char)\n this.opentui.symbols.editBufferInsertChar(buffer, charBytes, charBytes.length)\n }\n\n public editBufferInsertText(buffer: Pointer, text: string): void {\n const textBytes = this.encoder.encode(text)\n this.opentui.symbols.editBufferInsertText(buffer, textBytes, textBytes.length)\n }\n\n public editBufferDeleteChar(buffer: Pointer): void {\n this.opentui.symbols.editBufferDeleteChar(buffer)\n }\n\n public editBufferDeleteCharBackward(buffer: Pointer): void {\n this.opentui.symbols.editBufferDeleteCharBackward(buffer)\n }\n\n public editBufferDeleteRange(\n buffer: Pointer,\n startLine: number,\n startCol: number,\n endLine: number,\n endCol: number,\n ): void {\n this.opentui.symbols.editBufferDeleteRange(buffer, startLine, startCol, endLine, endCol)\n }\n\n public editBufferNewLine(buffer: Pointer): void {\n this.opentui.symbols.editBufferNewLine(buffer)\n }\n\n public editBufferDeleteLine(buffer: Pointer): void {\n this.opentui.symbols.editBufferDeleteLine(buffer)\n }\n\n public editBufferMoveCursorLeft(buffer: Pointer): void {\n this.opentui.symbols.editBufferMoveCursorLeft(buffer)\n }\n\n public editBufferMoveCursorRight(buffer: Pointer): void {\n this.opentui.symbols.editBufferMoveCursorRight(buffer)\n }\n\n public editBufferMoveCursorUp(buffer: Pointer): void {\n this.opentui.symbols.editBufferMoveCursorUp(buffer)\n }\n\n public editBufferMoveCursorDown(buffer: Pointer): void {\n this.opentui.symbols.editBufferMoveCursorDown(buffer)\n }\n\n public editBufferGotoLine(buffer: Pointer, line: number): void {\n this.opentui.symbols.editBufferGotoLine(buffer, line)\n }\n\n public editBufferSetCursor(buffer: Pointer, line: number, byteOffset: number): void {\n this.opentui.symbols.editBufferSetCursor(buffer, line, byteOffset)\n }\n\n public editBufferSetCursorToLineCol(buffer: Pointer, line: number, col: number): void {\n this.opentui.symbols.editBufferSetCursorToLineCol(buffer, line, col)\n }\n\n public editBufferSetCursorByOffset(buffer: Pointer, offset: number): void {\n this.opentui.symbols.editBufferSetCursorByOffset(buffer, offset)\n }\n\n public editBufferGetCursorPosition(buffer: Pointer): LogicalCursor {\n const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n this.opentui.symbols.editBufferGetCursorPosition(buffer, ptr(cursorBuffer))\n return LogicalCursorStruct.unpack(cursorBuffer)\n }\n\n public editBufferGetId(buffer: Pointer): number {\n return this.opentui.symbols.editBufferGetId(buffer)\n }\n\n public editBufferGetTextBuffer(buffer: Pointer): Pointer {\n const result = this.opentui.symbols.editBufferGetTextBuffer(buffer)\n if (!result) {\n throw new Error(\"Failed to get TextBuffer from EditBuffer\")\n }\n return result\n }\n\n public editBufferDebugLogRope(buffer: Pointer): void {\n this.opentui.symbols.editBufferDebugLogRope(buffer)\n }\n\n public editBufferUndo(buffer: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editBufferUndo(buffer, ptr(outBuffer), maxLength)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editBufferRedo(buffer: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editBufferRedo(buffer, ptr(outBuffer), maxLength)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editBufferCanUndo(buffer: Pointer): boolean {\n return this.opentui.symbols.editBufferCanUndo(buffer)\n }\n\n public editBufferCanRedo(buffer: Pointer): boolean {\n return this.opentui.symbols.editBufferCanRedo(buffer)\n }\n\n public editBufferClearHistory(buffer: Pointer): void {\n this.opentui.symbols.editBufferClearHistory(buffer)\n }\n\n public editBufferClear(buffer: Pointer): void {\n this.opentui.symbols.editBufferClear(buffer)\n }\n\n public editBufferGetNextWordBoundary(buffer: Pointer): LogicalCursor {\n const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n this.opentui.symbols.editBufferGetNextWordBoundary(buffer, ptr(cursorBuffer))\n return LogicalCursorStruct.unpack(cursorBuffer)\n }\n\n public editBufferGetPrevWordBoundary(buffer: Pointer): LogicalCursor {\n const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n this.opentui.symbols.editBufferGetPrevWordBoundary(buffer, ptr(cursorBuffer))\n return LogicalCursorStruct.unpack(cursorBuffer)\n }\n\n public editBufferGetEOL(buffer: Pointer): LogicalCursor {\n const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n this.opentui.symbols.editBufferGetEOL(buffer, ptr(cursorBuffer))\n return LogicalCursorStruct.unpack(cursorBuffer)\n }\n\n public editBufferOffsetToPosition(buffer: Pointer, offset: number): LogicalCursor | null {\n const cursorBuffer = new ArrayBuffer(LogicalCursorStruct.size)\n const success = this.opentui.symbols.editBufferOffsetToPosition(buffer, offset, ptr(cursorBuffer))\n if (!success) return null\n return LogicalCursorStruct.unpack(cursorBuffer)\n }\n\n public editBufferPositionToOffset(buffer: Pointer, row: number, col: number): number {\n return this.opentui.symbols.editBufferPositionToOffset(buffer, row, col)\n }\n\n public editBufferGetLineStartOffset(buffer: Pointer, row: number): number {\n return this.opentui.symbols.editBufferGetLineStartOffset(buffer, row)\n }\n\n public editBufferGetTextRange(\n buffer: Pointer,\n startOffset: number,\n endOffset: number,\n maxLength: number,\n ): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editBufferGetTextRange(\n buffer,\n startOffset,\n endOffset,\n ptr(outBuffer),\n maxLength,\n )\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editBufferGetTextRangeByCoords(\n buffer: Pointer,\n startRow: number,\n startCol: number,\n endRow: number,\n endCol: number,\n maxLength: number,\n ): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editBufferGetTextRangeByCoords(\n buffer,\n startRow,\n startCol,\n endRow,\n endCol,\n ptr(outBuffer),\n maxLength,\n )\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n // EditorView selection and editing implementations\n public editorViewSetSelection(\n view: Pointer,\n start: number,\n end: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n ): void {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n this.opentui.symbols.editorViewSetSelection(view, start, end, bg, fg)\n }\n\n public editorViewResetSelection(view: Pointer): void {\n this.opentui.symbols.editorViewResetSelection(view)\n }\n\n public editorViewGetSelection(view: Pointer): { start: number; end: number } | null {\n const packedInfo = this.opentui.symbols.editorViewGetSelection(view)\n if (packedInfo === 0xffff_ffff_ffff_ffffn) {\n return null\n }\n const start = Number(packedInfo >> 32n)\n const end = Number(packedInfo & 0xffff_ffffn)\n return { start, end }\n }\n\n public editorViewSetLocalSelection(\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n updateCursor: boolean,\n followCursor: boolean,\n ): boolean {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n return this.opentui.symbols.editorViewSetLocalSelection(\n view,\n anchorX,\n anchorY,\n focusX,\n focusY,\n bg,\n fg,\n updateCursor,\n followCursor,\n )\n }\n\n public editorViewUpdateSelection(view: Pointer, end: number, bgColor: RGBA | null, fgColor: RGBA | null): void {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n this.opentui.symbols.editorViewUpdateSelection(view, end, bg, fg)\n }\n\n public editorViewUpdateLocalSelection(\n view: Pointer,\n anchorX: number,\n anchorY: number,\n focusX: number,\n focusY: number,\n bgColor: RGBA | null,\n fgColor: RGBA | null,\n updateCursor: boolean,\n followCursor: boolean,\n ): boolean {\n const bg = bgColor ? bgColor.buffer : null\n const fg = fgColor ? fgColor.buffer : null\n return this.opentui.symbols.editorViewUpdateLocalSelection(\n view,\n anchorX,\n anchorY,\n focusX,\n focusY,\n bg,\n fg,\n updateCursor,\n followCursor,\n )\n }\n\n public editorViewResetLocalSelection(view: Pointer): void {\n this.opentui.symbols.editorViewResetLocalSelection(view)\n }\n\n public editorViewGetSelectedTextBytes(view: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editorViewGetSelectedTextBytes(view, ptr(outBuffer), maxLength)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editorViewGetCursor(view: Pointer): { row: number; col: number } {\n const row = new Uint32Array(1)\n const col = new Uint32Array(1)\n this.opentui.symbols.editorViewGetCursor(view, ptr(row), ptr(col))\n return { row: row[0], col: col[0] }\n }\n\n public editorViewGetText(view: Pointer, maxLength: number): Uint8Array | null {\n const outBuffer = new Uint8Array(maxLength)\n const actualLen = this.opentui.symbols.editorViewGetText(view, ptr(outBuffer), maxLength)\n const len = typeof actualLen === \"bigint\" ? Number(actualLen) : actualLen\n if (len === 0) return null\n return outBuffer.slice(0, len)\n }\n\n public editorViewGetVisualCursor(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetVisualCursor(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public editorViewMoveUpVisual(view: Pointer): void {\n this.opentui.symbols.editorViewMoveUpVisual(view)\n }\n\n public editorViewMoveDownVisual(view: Pointer): void {\n this.opentui.symbols.editorViewMoveDownVisual(view)\n }\n\n public editorViewDeleteSelectedText(view: Pointer): void {\n this.opentui.symbols.editorViewDeleteSelectedText(view)\n }\n\n public editorViewSetCursorByOffset(view: Pointer, offset: number): void {\n this.opentui.symbols.editorViewSetCursorByOffset(view, offset)\n }\n\n public editorViewGetNextWordBoundary(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetNextWordBoundary(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public editorViewGetPrevWordBoundary(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetPrevWordBoundary(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public editorViewGetEOL(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetEOL(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public editorViewGetVisualSOL(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetVisualSOL(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public editorViewGetVisualEOL(view: Pointer): VisualCursor {\n const cursorBuffer = new ArrayBuffer(VisualCursorStruct.size)\n this.opentui.symbols.editorViewGetVisualEOL(view, ptr(cursorBuffer))\n return VisualCursorStruct.unpack(cursorBuffer)\n }\n\n public bufferPushScissorRect(buffer: Pointer, x: number, y: number, width: number, height: number): void {\n this.opentui.symbols.bufferPushScissorRect(buffer, x, y, width, height)\n }\n\n public bufferPopScissorRect(buffer: Pointer): void {\n this.opentui.symbols.bufferPopScissorRect(buffer)\n }\n\n public bufferClearScissorRects(buffer: Pointer): void {\n this.opentui.symbols.bufferClearScissorRects(buffer)\n }\n\n public bufferPushOpacity(buffer: Pointer, opacity: number): void {\n this.opentui.symbols.bufferPushOpacity(buffer, opacity)\n }\n\n public bufferPopOpacity(buffer: Pointer): void {\n this.opentui.symbols.bufferPopOpacity(buffer)\n }\n\n public bufferGetCurrentOpacity(buffer: Pointer): number {\n return this.opentui.symbols.bufferGetCurrentOpacity(buffer)\n }\n\n public bufferClearOpacity(buffer: Pointer): void {\n this.opentui.symbols.bufferClearOpacity(buffer)\n }\n\n public getTerminalCapabilities(renderer: Pointer) {\n const capsBuffer = new ArrayBuffer(TerminalCapabilitiesStruct.size)\n this.opentui.symbols.getTerminalCapabilities(renderer, ptr(capsBuffer))\n\n const caps = TerminalCapabilitiesStruct.unpack(capsBuffer)\n\n return {\n kitty_keyboard: caps.kitty_keyboard,\n kitty_graphics: caps.kitty_graphics,\n rgb: caps.rgb,\n unicode: caps.unicode,\n sgr_pixels: caps.sgr_pixels,\n color_scheme_updates: caps.color_scheme_updates,\n explicit_width: caps.explicit_width,\n scaled_text: caps.scaled_text,\n sixel: caps.sixel,\n focus_tracking: caps.focus_tracking,\n sync: caps.sync,\n bracketed_paste: caps.bracketed_paste,\n hyperlinks: caps.hyperlinks,\n osc52: caps.osc52,\n explicit_cursor_positioning: caps.explicit_cursor_positioning,\n terminal: {\n name: caps.term_name ?? \"\",\n version: caps.term_version ?? \"\",\n from_xtversion: caps.term_from_xtversion,\n },\n }\n }\n\n public processCapabilityResponse(renderer: Pointer, response: string): void {\n const responseBytes = this.encoder.encode(response)\n this.opentui.symbols.processCapabilityResponse(renderer, responseBytes, responseBytes.length)\n }\n\n public encodeUnicode(\n text: string,\n widthMethod: WidthMethod,\n ): { ptr: Pointer; data: Array<{ width: number; char: number }> } | null {\n const textBytes = this.encoder.encode(text)\n const widthMethodCode = widthMethod === \"wcwidth\" ? 0 : 1\n\n const outPtrBuffer = new ArrayBuffer(8) // Pointer size\n const outLenBuffer = new ArrayBuffer(8) // usize\n\n const success = this.opentui.symbols.encodeUnicode(\n textBytes,\n textBytes.length,\n ptr(outPtrBuffer),\n ptr(outLenBuffer),\n widthMethodCode,\n )\n\n if (!success) {\n return null\n }\n\n const outPtrView = new BigUint64Array(outPtrBuffer)\n const outLenView = new BigUint64Array(outLenBuffer)\n\n const resultPtr = Number(outPtrView[0]) as Pointer\n const resultLen = Number(outLenView[0])\n\n if (resultLen === 0) {\n return { ptr: resultPtr, data: [] }\n }\n\n // Convert pointer to ArrayBuffer and use EncodedCharStruct to unpack the list\n const byteLen = resultLen * EncodedCharStruct.size\n const raw = toArrayBuffer(resultPtr, 0, byteLen)\n const data = EncodedCharStruct.unpackList(raw, resultLen)\n\n return { ptr: resultPtr, data }\n }\n\n public freeUnicode(encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }): void {\n this.opentui.symbols.freeUnicode(encoded.ptr, encoded.data.length)\n }\n\n public bufferDrawChar(\n buffer: Pointer,\n char: number,\n x: number,\n y: number,\n fg: RGBA,\n bg: RGBA,\n attributes: number = 0,\n ): void {\n this.opentui.symbols.bufferDrawChar(buffer, char, x, y, fg.buffer, bg.buffer, attributes)\n }\n\n public registerNativeSpanFeedStream(stream: Pointer, handler: NativeSpanFeedEventHandler): void {\n const callback = this.ensureNativeSpanFeedCallback()\n this.nativeSpanFeedHandlers.set(toPointer(stream), handler)\n this.opentui.symbols.streamSetCallback(stream, callback.ptr)\n }\n\n public unregisterNativeSpanFeedStream(stream: Pointer): void {\n this.opentui.symbols.streamSetCallback(stream, null)\n this.nativeSpanFeedHandlers.delete(toPointer(stream))\n }\n\n public createNativeSpanFeed(options?: NativeSpanFeedOptions | null): Pointer {\n const optionsBuffer = options == null ? null : NativeSpanFeedOptionsStruct.pack(options)\n const streamPtr = this.opentui.symbols.createNativeSpanFeed(optionsBuffer ? ptr(optionsBuffer) : null)\n if (!streamPtr) {\n throw new Error(\"Failed to create stream\")\n }\n return toPointer(streamPtr)\n }\n\n public attachNativeSpanFeed(stream: Pointer): number {\n return this.opentui.symbols.attachNativeSpanFeed(stream)\n }\n\n public destroyNativeSpanFeed(stream: Pointer): void {\n this.opentui.symbols.destroyNativeSpanFeed(stream)\n this.nativeSpanFeedHandlers.delete(toPointer(stream))\n }\n\n public streamWrite(stream: Pointer, data: Uint8Array | string): number {\n const bytes = typeof data === \"string\" ? this.encoder.encode(data) : data\n return this.opentui.symbols.streamWrite(stream, ptr(bytes), bytes.length)\n }\n\n public streamCommit(stream: Pointer): number {\n return this.opentui.symbols.streamCommit(stream)\n }\n\n public streamDrainSpans(stream: Pointer, outBuffer: Uint8Array, maxSpans: number): number {\n const count = this.opentui.symbols.streamDrainSpans(stream, ptr(outBuffer), maxSpans)\n return toNumber(count)\n }\n\n public streamClose(stream: Pointer): number {\n return this.opentui.symbols.streamClose(stream)\n }\n\n public streamSetOptions(stream: Pointer, options: NativeSpanFeedOptions): number {\n const optionsBuffer = NativeSpanFeedOptionsStruct.pack(options)\n return this.opentui.symbols.streamSetOptions(stream, ptr(optionsBuffer))\n }\n\n public streamGetStats(stream: Pointer): NativeSpanFeedStats | null {\n const statsBuffer = new ArrayBuffer(NativeSpanFeedStatsStruct.size)\n const status = this.opentui.symbols.streamGetStats(stream, ptr(statsBuffer))\n if (status !== 0) {\n return null\n }\n const stats = NativeSpanFeedStatsStruct.unpack(statsBuffer)\n return {\n bytesWritten: typeof stats.bytesWritten === \"bigint\" ? stats.bytesWritten : BigInt(stats.bytesWritten),\n spansCommitted: typeof stats.spansCommitted === \"bigint\" ? stats.spansCommitted : BigInt(stats.spansCommitted),\n chunks: stats.chunks,\n pendingSpans: stats.pendingSpans,\n }\n }\n\n public streamReserve(stream: Pointer, minLen: number): { status: number; info: ReserveInfo | null } {\n const reserveBuffer = new ArrayBuffer(ReserveInfoStruct.size)\n const status = this.opentui.symbols.streamReserve(stream, minLen, ptr(reserveBuffer))\n if (status !== 0) {\n return { status, info: null }\n }\n return { status, info: ReserveInfoStruct.unpack(reserveBuffer) }\n }\n\n public streamCommitReserved(stream: Pointer, length: number): number {\n return this.opentui.symbols.streamCommitReserved(stream, length)\n }\n\n public createSyntaxStyle(): Pointer {\n const stylePtr = this.opentui.symbols.createSyntaxStyle()\n if (!stylePtr) {\n throw new Error(\"Failed to create SyntaxStyle\")\n }\n return stylePtr\n }\n\n public destroySyntaxStyle(style: Pointer): void {\n this.opentui.symbols.destroySyntaxStyle(style)\n }\n\n public syntaxStyleRegister(\n style: Pointer,\n name: string,\n fg: RGBA | null,\n bg: RGBA | null,\n attributes: number,\n ): number {\n const nameBytes = this.encoder.encode(name)\n const fgPtr = fg ? fg.buffer : null\n const bgPtr = bg ? bg.buffer : null\n return this.opentui.symbols.syntaxStyleRegister(style, nameBytes, nameBytes.length, fgPtr, bgPtr, attributes)\n }\n\n public syntaxStyleResolveByName(style: Pointer, name: string): number | null {\n const nameBytes = this.encoder.encode(name)\n const id = this.opentui.symbols.syntaxStyleResolveByName(style, nameBytes, nameBytes.length)\n return id === 0 ? null : id\n }\n\n public syntaxStyleGetStyleCount(style: Pointer): number {\n const result = this.opentui.symbols.syntaxStyleGetStyleCount(style)\n return typeof result === \"bigint\" ? Number(result) : result\n }\n\n public editorViewSetPlaceholderStyledText(\n view: Pointer,\n chunks: Array<{ text: string; fg?: RGBA | null; bg?: RGBA | null; attributes?: number }>,\n ): void {\n const nonEmptyChunks = chunks.filter((c) => c.text.length > 0)\n if (nonEmptyChunks.length === 0) {\n this.opentui.symbols.editorViewSetPlaceholderStyledText(view, null, 0)\n return\n }\n\n const chunksBuffer = StyledChunkStruct.packList(nonEmptyChunks)\n this.opentui.symbols.editorViewSetPlaceholderStyledText(view, ptr(chunksBuffer), nonEmptyChunks.length)\n }\n\n public editorViewSetTabIndicator(view: Pointer, indicator: number): void {\n this.opentui.symbols.editorViewSetTabIndicator(view, indicator)\n }\n\n public editorViewSetTabIndicatorColor(view: Pointer, color: RGBA): void {\n this.opentui.symbols.editorViewSetTabIndicatorColor(view, color.buffer)\n }\n\n public onNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n this._nativeEvents.on(name, handler)\n }\n\n public onceNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n this._nativeEvents.once(name, handler)\n }\n\n public offNativeEvent(name: string, handler: (data: ArrayBuffer) => void): void {\n this._nativeEvents.off(name, handler)\n }\n\n public onAnyNativeEvent(handler: (name: string, data: ArrayBuffer) => void): void {\n this._anyEventHandlers.push(handler)\n }\n}\n\nlet opentuiLibPath: string | undefined\nlet opentuiLib: RenderLib | undefined\n\nexport function setRenderLibPath(libPath: string) {\n if (opentuiLibPath !== libPath) {\n opentuiLibPath = libPath\n opentuiLib = undefined\n }\n}\n\nexport function resolveRenderLib(): RenderLib {\n if (!opentuiLib) {\n try {\n opentuiLib = new FFIRenderLib(opentuiLibPath)\n } catch (error) {\n throw new Error(\n `Failed to initialize OpenTUI render library: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n )\n }\n }\n return opentuiLib\n}\n\n// Try eager loading\ntry {\n opentuiLib = new FFIRenderLib(opentuiLibPath)\n} catch (error) {}\n",
45
- "import { RGBA } from \"./lib\"\nimport { resolveRenderLib, type RenderLib } from \"./zig\"\nimport { type Pointer, toArrayBuffer, ptr } from \"bun:ffi\"\nimport { type BorderStyle, type BorderSides, BorderCharArrays, parseBorderStyle } from \"./lib/index.js\"\nimport { TargetChannel, type WidthMethod, type CapturedSpan, type CapturedLine } from \"./types.js\"\nimport type { TextBufferView } from \"./text-buffer-view.js\"\nimport type { EditorView } from \"./editor-view.js\"\n\n// Pack drawing options into a single u32\n// bits 0-3: borderSides, bit 4: shouldFill, bits 5-6: titleAlignment\nfunction packDrawOptions(\n border: boolean | BorderSides[],\n shouldFill: boolean,\n titleAlignment: \"left\" | \"center\" | \"right\",\n): number {\n let packed = 0\n\n if (border === true) {\n packed |= 0b1111 // All sides\n } else if (Array.isArray(border)) {\n if (border.includes(\"top\")) packed |= 0b1000\n if (border.includes(\"right\")) packed |= 0b0100\n if (border.includes(\"bottom\")) packed |= 0b0010\n if (border.includes(\"left\")) packed |= 0b0001\n }\n\n if (shouldFill) {\n packed |= 1 << 4\n }\n\n const alignmentMap: Record<string, number> = {\n left: 0,\n center: 1,\n right: 2,\n }\n const alignment = alignmentMap[titleAlignment]\n packed |= alignment << 5\n\n return packed\n}\n\nexport class OptimizedBuffer {\n private static fbIdCounter = 0\n public id: string\n public lib: RenderLib\n private bufferPtr: Pointer\n private _width: number\n private _height: number\n private _widthMethod: WidthMethod\n public respectAlpha: boolean = false\n private _rawBuffers: {\n char: Uint32Array\n fg: Float32Array\n bg: Float32Array\n attributes: Uint32Array\n } | null = null\n private _destroyed: boolean = false\n\n get ptr(): Pointer {\n return this.bufferPtr\n }\n\n // Fail loud and clear\n // Instead of trying to return values that could work or not,\n // this at least will show a stack trace to know where the call to a destroyed Buffer was made\n private guard(): void {\n if (this._destroyed) throw new Error(`Buffer ${this.id} is destroyed`)\n }\n\n get buffers(): {\n char: Uint32Array\n fg: Float32Array\n bg: Float32Array\n attributes: Uint32Array\n } {\n this.guard()\n if (this._rawBuffers === null) {\n const size = this._width * this._height\n const charPtr = this.lib.bufferGetCharPtr(this.bufferPtr)\n const fgPtr = this.lib.bufferGetFgPtr(this.bufferPtr)\n const bgPtr = this.lib.bufferGetBgPtr(this.bufferPtr)\n const attributesPtr = this.lib.bufferGetAttributesPtr(this.bufferPtr)\n\n this._rawBuffers = {\n char: new Uint32Array(toArrayBuffer(charPtr, 0, size * 4)),\n fg: new Float32Array(toArrayBuffer(fgPtr, 0, size * 4 * 4)),\n bg: new Float32Array(toArrayBuffer(bgPtr, 0, size * 4 * 4)),\n attributes: new Uint32Array(toArrayBuffer(attributesPtr, 0, size * 4)),\n }\n }\n\n return this._rawBuffers\n }\n\n constructor(\n lib: RenderLib,\n ptr: Pointer,\n width: number,\n height: number,\n options: { respectAlpha?: boolean; id?: string; widthMethod?: WidthMethod },\n ) {\n this.id = options.id || `fb_${OptimizedBuffer.fbIdCounter++}`\n this.lib = lib\n this.respectAlpha = options.respectAlpha || false\n this._width = width\n this._height = height\n this._widthMethod = options.widthMethod || \"unicode\"\n this.bufferPtr = ptr\n }\n\n static create(\n width: number,\n height: number,\n widthMethod: WidthMethod,\n options: { respectAlpha?: boolean; id?: string } = {},\n ): OptimizedBuffer {\n const lib = resolveRenderLib()\n const respectAlpha = options.respectAlpha || false\n const id = options.id && options.id.trim() !== \"\" ? options.id : \"unnamed buffer\"\n const buffer = lib.createOptimizedBuffer(width, height, widthMethod, respectAlpha, id)\n return buffer\n }\n\n public get widthMethod(): WidthMethod {\n return this._widthMethod\n }\n\n public get width(): number {\n return this._width\n }\n\n public get height(): number {\n return this._height\n }\n\n public setRespectAlpha(respectAlpha: boolean): void {\n this.guard()\n this.lib.bufferSetRespectAlpha(this.bufferPtr, respectAlpha)\n this.respectAlpha = respectAlpha\n }\n\n public getNativeId(): string {\n this.guard()\n return this.lib.bufferGetId(this.bufferPtr)\n }\n\n public getRealCharBytes(addLineBreaks: boolean = false): Uint8Array {\n this.guard()\n const realSize = this.lib.bufferGetRealCharSize(this.bufferPtr)\n const outputBuffer = new Uint8Array(realSize)\n const bytesWritten = this.lib.bufferWriteResolvedChars(this.bufferPtr, outputBuffer, addLineBreaks)\n return outputBuffer.slice(0, bytesWritten)\n }\n\n public getSpanLines(): CapturedLine[] {\n this.guard()\n const { char, fg, bg, attributes } = this.buffers\n const lines: CapturedLine[] = []\n\n const CHAR_FLAG_CONTINUATION = 0xc0000000 | 0\n const CHAR_FLAG_MASK = 0xc0000000 | 0\n\n const realTextBytes = this.getRealCharBytes(true)\n const realTextLines = new TextDecoder().decode(realTextBytes).split(\"\\n\")\n\n for (let y = 0; y < this._height; y++) {\n const spans: CapturedSpan[] = []\n let currentSpan: CapturedSpan | null = null\n\n const lineChars = [...(realTextLines[y] || \"\")]\n let charIdx = 0\n\n for (let x = 0; x < this._width; x++) {\n const i = y * this._width + x\n const cp = char[i]\n const cellFg = RGBA.fromValues(fg[i * 4], fg[i * 4 + 1], fg[i * 4 + 2], fg[i * 4 + 3])\n const cellBg = RGBA.fromValues(bg[i * 4], bg[i * 4 + 1], bg[i * 4 + 2], bg[i * 4 + 3])\n const cellAttrs = attributes[i] & 0xff\n\n // Continuation cells are placeholders for wide characters (emojis, CJK)\n const isContinuation = (cp & CHAR_FLAG_MASK) === CHAR_FLAG_CONTINUATION\n const cellChar = isContinuation ? \"\" : (lineChars[charIdx++] ?? \" \")\n\n // Check if this cell continues the current span\n if (\n currentSpan &&\n currentSpan.fg.equals(cellFg) &&\n currentSpan.bg.equals(cellBg) &&\n currentSpan.attributes === cellAttrs\n ) {\n currentSpan.text += cellChar\n currentSpan.width += 1\n } else {\n // Start a new span\n if (currentSpan) {\n spans.push(currentSpan)\n }\n currentSpan = {\n text: cellChar,\n fg: cellFg,\n bg: cellBg,\n attributes: cellAttrs,\n width: 1,\n }\n }\n }\n\n // Push the last span\n if (currentSpan) {\n spans.push(currentSpan)\n }\n\n lines.push({ spans })\n }\n\n return lines\n }\n\n public clear(bg: RGBA = RGBA.fromValues(0, 0, 0, 1)): void {\n this.guard()\n this.lib.bufferClear(this.bufferPtr, bg)\n }\n\n public setCell(x: number, y: number, char: string, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n this.guard()\n this.lib.bufferSetCell(this.bufferPtr, x, y, char, fg, bg, attributes)\n }\n\n public setCellWithAlphaBlending(\n x: number,\n y: number,\n char: string,\n fg: RGBA,\n bg: RGBA,\n attributes: number = 0,\n ): void {\n this.guard()\n this.lib.bufferSetCellWithAlphaBlending(this.bufferPtr, x, y, char, fg, bg, attributes)\n }\n\n public drawText(\n text: string,\n x: number,\n y: number,\n fg: RGBA,\n bg?: RGBA,\n attributes: number = 0,\n selection?: { start: number; end: number; bgColor?: RGBA; fgColor?: RGBA } | null,\n ): void {\n this.guard()\n if (!selection) {\n this.lib.bufferDrawText(this.bufferPtr, text, x, y, fg, bg, attributes)\n return\n }\n\n const { start, end } = selection\n\n let selectionBg: RGBA\n let selectionFg: RGBA\n\n if (selection.bgColor) {\n selectionBg = selection.bgColor\n selectionFg = selection.fgColor || fg\n } else {\n const defaultBg = bg || RGBA.fromValues(0, 0, 0, 0)\n selectionFg = defaultBg.a > 0 ? defaultBg : RGBA.fromValues(0, 0, 0, 1)\n selectionBg = fg\n }\n\n if (start > 0) {\n const beforeText = text.slice(0, start)\n this.lib.bufferDrawText(this.bufferPtr, beforeText, x, y, fg, bg, attributes)\n }\n\n if (end > start) {\n const selectedText = text.slice(start, end)\n this.lib.bufferDrawText(this.bufferPtr, selectedText, x + start, y, selectionFg, selectionBg, attributes)\n }\n\n if (end < text.length) {\n const afterText = text.slice(end)\n this.lib.bufferDrawText(this.bufferPtr, afterText, x + end, y, fg, bg, attributes)\n }\n }\n\n public fillRect(x: number, y: number, width: number, height: number, bg: RGBA): void {\n this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg)\n }\n\n public colorMatrix(\n matrix: Float32Array,\n cellMask: Float32Array,\n strength: number = 1.0,\n target: TargetChannel = TargetChannel.Both,\n ): void {\n this.guard()\n if (matrix.length !== 16) throw new RangeError(`colorMatrix matrix must have length 16, got ${matrix.length}`)\n const cellMaskCount = Math.floor(cellMask.length / 3)\n this.lib.bufferColorMatrix(this.bufferPtr, ptr(matrix), ptr(cellMask), cellMaskCount, strength, target)\n }\n\n public colorMatrixUniform(\n matrix: Float32Array,\n strength: number = 1.0,\n target: TargetChannel = TargetChannel.Both,\n ): void {\n this.guard()\n if (matrix.length !== 16)\n throw new RangeError(`colorMatrixUniform matrix must have length 16, got ${matrix.length}`)\n if (strength === 0.0) return\n this.lib.bufferColorMatrixUniform(this.bufferPtr, ptr(matrix), strength, target)\n }\n\n public drawFrameBuffer(\n destX: number,\n destY: number,\n frameBuffer: OptimizedBuffer,\n sourceX?: number,\n sourceY?: number,\n sourceWidth?: number,\n sourceHeight?: number,\n ): void {\n this.guard()\n this.lib.drawFrameBuffer(this.bufferPtr, destX, destY, frameBuffer.ptr, sourceX, sourceY, sourceWidth, sourceHeight)\n }\n\n public destroy(): void {\n if (this._destroyed) return\n this._destroyed = true\n this.lib.destroyOptimizedBuffer(this.bufferPtr)\n }\n\n public drawTextBuffer(textBufferView: TextBufferView, x: number, y: number): void {\n this.guard()\n this.lib.bufferDrawTextBufferView(this.bufferPtr, textBufferView.ptr, x, y)\n }\n\n public drawEditorView(editorView: EditorView, x: number, y: number): void {\n this.guard()\n this.lib.bufferDrawEditorView(this.bufferPtr, editorView.ptr, x, y)\n }\n\n public drawSuperSampleBuffer(\n x: number,\n y: number,\n pixelDataPtr: Pointer,\n pixelDataLength: number,\n format: \"bgra8unorm\" | \"rgba8unorm\",\n alignedBytesPerRow: number,\n ): void {\n this.guard()\n this.lib.bufferDrawSuperSampleBuffer(\n this.bufferPtr,\n x,\n y,\n pixelDataPtr,\n pixelDataLength,\n format,\n alignedBytesPerRow,\n )\n }\n\n public drawPackedBuffer(\n dataPtr: Pointer,\n dataLen: number,\n posX: number,\n posY: number,\n terminalWidthCells: number,\n terminalHeightCells: number,\n ): void {\n this.guard()\n this.lib.bufferDrawPackedBuffer(\n this.bufferPtr,\n dataPtr,\n dataLen,\n posX,\n posY,\n terminalWidthCells,\n terminalHeightCells,\n )\n }\n\n public drawGrayscaleBuffer(\n posX: number,\n posY: number,\n intensities: Float32Array,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null = null,\n bg: RGBA | null = null,\n ): void {\n this.guard()\n this.lib.bufferDrawGrayscaleBuffer(this.bufferPtr, posX, posY, ptr(intensities), srcWidth, srcHeight, fg, bg)\n }\n\n public drawGrayscaleBufferSupersampled(\n posX: number,\n posY: number,\n intensities: Float32Array,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null = null,\n bg: RGBA | null = null,\n ): void {\n this.guard()\n this.lib.bufferDrawGrayscaleBufferSupersampled(\n this.bufferPtr,\n posX,\n posY,\n ptr(intensities),\n srcWidth,\n srcHeight,\n fg,\n bg,\n )\n }\n\n public resize(width: number, height: number): void {\n this.guard()\n if (this._width === width && this._height === height) return\n\n this._width = width\n this._height = height\n this._rawBuffers = null\n\n this.lib.bufferResize(this.bufferPtr, width, height)\n }\n\n public drawBox(options: {\n x: number\n y: number\n width: number\n height: number\n borderStyle?: BorderStyle\n customBorderChars?: Uint32Array\n border: boolean | BorderSides[]\n borderColor: RGBA\n backgroundColor: RGBA\n shouldFill?: boolean\n title?: string\n titleAlignment?: \"left\" | \"center\" | \"right\"\n }): void {\n this.guard()\n const style = parseBorderStyle(options.borderStyle, \"single\")\n const borderChars: Uint32Array = options.customBorderChars ?? BorderCharArrays[style]\n\n const packedOptions = packDrawOptions(options.border, options.shouldFill ?? false, options.titleAlignment || \"left\")\n\n this.lib.bufferDrawBox(\n this.bufferPtr,\n options.x,\n options.y,\n options.width,\n options.height,\n borderChars,\n packedOptions,\n options.borderColor,\n options.backgroundColor,\n options.title ?? null,\n )\n }\n\n public pushScissorRect(x: number, y: number, width: number, height: number): void {\n this.guard()\n this.lib.bufferPushScissorRect(this.bufferPtr, x, y, width, height)\n }\n\n public popScissorRect(): void {\n this.guard()\n this.lib.bufferPopScissorRect(this.bufferPtr)\n }\n\n public clearScissorRects(): void {\n this.guard()\n this.lib.bufferClearScissorRects(this.bufferPtr)\n }\n\n public pushOpacity(opacity: number): void {\n this.guard()\n this.lib.bufferPushOpacity(this.bufferPtr, Math.max(0, Math.min(1, opacity)))\n }\n\n public popOpacity(): void {\n this.guard()\n this.lib.bufferPopOpacity(this.bufferPtr)\n }\n\n public getCurrentOpacity(): number {\n this.guard()\n return this.lib.bufferGetCurrentOpacity(this.bufferPtr)\n }\n\n public clearOpacity(): void {\n this.guard()\n this.lib.bufferClearOpacity(this.bufferPtr)\n }\n\n public encodeUnicode(text: string): { ptr: Pointer; data: Array<{ width: number; char: number }> } | null {\n this.guard()\n return this.lib.encodeUnicode(text, this._widthMethod)\n }\n\n public freeUnicode(encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }): void {\n this.guard()\n this.lib.freeUnicode(encoded)\n }\n\n public drawGrid(options: {\n borderChars: Uint32Array\n borderFg: RGBA\n borderBg: RGBA\n columnOffsets: Int32Array\n rowOffsets: Int32Array\n drawInner: boolean\n drawOuter: boolean\n }): void {\n this.guard()\n\n const columnCount = Math.max(0, options.columnOffsets.length - 1)\n const rowCount = Math.max(0, options.rowOffsets.length - 1)\n\n this.lib.bufferDrawGrid(\n this.bufferPtr,\n options.borderChars,\n options.borderFg,\n options.borderBg,\n options.columnOffsets,\n columnCount,\n options.rowOffsets,\n rowCount,\n {\n drawInner: options.drawInner,\n drawOuter: options.drawOuter,\n },\n )\n }\n\n public drawChar(char: number, x: number, y: number, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n this.guard()\n this.lib.bufferDrawChar(this.bufferPtr, char, x, y, fg, bg, attributes)\n }\n}\n",
45
+ "import { RGBA } from \"./lib/index.js\"\nimport { resolveRenderLib, type RenderLib } from \"./zig.js\"\nimport { type Pointer, toArrayBuffer, ptr } from \"bun:ffi\"\nimport { type BorderStyle, type BorderSides, BorderCharArrays, parseBorderStyle } from \"./lib/index.js\"\nimport { TargetChannel, type WidthMethod, type CapturedSpan, type CapturedLine } from \"./types.js\"\nimport type { TextBufferView } from \"./text-buffer-view.js\"\nimport type { EditorView } from \"./editor-view.js\"\n\n// Pack drawing options into a single u32\n// bits 0-3: borderSides, bit 4: shouldFill, bits 5-6: titleAlignment\nfunction packDrawOptions(\n border: boolean | BorderSides[],\n shouldFill: boolean,\n titleAlignment: \"left\" | \"center\" | \"right\",\n): number {\n let packed = 0\n\n if (border === true) {\n packed |= 0b1111 // All sides\n } else if (Array.isArray(border)) {\n if (border.includes(\"top\")) packed |= 0b1000\n if (border.includes(\"right\")) packed |= 0b0100\n if (border.includes(\"bottom\")) packed |= 0b0010\n if (border.includes(\"left\")) packed |= 0b0001\n }\n\n if (shouldFill) {\n packed |= 1 << 4\n }\n\n const alignmentMap: Record<string, number> = {\n left: 0,\n center: 1,\n right: 2,\n }\n const alignment = alignmentMap[titleAlignment]\n packed |= alignment << 5\n\n return packed\n}\n\nexport class OptimizedBuffer {\n private static fbIdCounter = 0\n public id: string\n public lib: RenderLib\n private bufferPtr: Pointer\n private _width: number\n private _height: number\n private _widthMethod: WidthMethod\n public respectAlpha: boolean = false\n private _rawBuffers: {\n char: Uint32Array\n fg: Float32Array\n bg: Float32Array\n attributes: Uint32Array\n } | null = null\n private _destroyed: boolean = false\n\n get ptr(): Pointer {\n return this.bufferPtr\n }\n\n // Fail loud and clear\n // Instead of trying to return values that could work or not,\n // this at least will show a stack trace to know where the call to a destroyed Buffer was made\n private guard(): void {\n if (this._destroyed) throw new Error(`Buffer ${this.id} is destroyed`)\n }\n\n get buffers(): {\n char: Uint32Array\n fg: Float32Array\n bg: Float32Array\n attributes: Uint32Array\n } {\n this.guard()\n if (this._rawBuffers === null) {\n const size = this._width * this._height\n const charPtr = this.lib.bufferGetCharPtr(this.bufferPtr)\n const fgPtr = this.lib.bufferGetFgPtr(this.bufferPtr)\n const bgPtr = this.lib.bufferGetBgPtr(this.bufferPtr)\n const attributesPtr = this.lib.bufferGetAttributesPtr(this.bufferPtr)\n\n this._rawBuffers = {\n char: new Uint32Array(toArrayBuffer(charPtr, 0, size * 4)),\n fg: new Float32Array(toArrayBuffer(fgPtr, 0, size * 4 * 4)),\n bg: new Float32Array(toArrayBuffer(bgPtr, 0, size * 4 * 4)),\n attributes: new Uint32Array(toArrayBuffer(attributesPtr, 0, size * 4)),\n }\n }\n\n return this._rawBuffers\n }\n\n constructor(\n lib: RenderLib,\n ptr: Pointer,\n width: number,\n height: number,\n options: { respectAlpha?: boolean; id?: string; widthMethod?: WidthMethod },\n ) {\n this.id = options.id || `fb_${OptimizedBuffer.fbIdCounter++}`\n this.lib = lib\n this.respectAlpha = options.respectAlpha || false\n this._width = width\n this._height = height\n this._widthMethod = options.widthMethod || \"unicode\"\n this.bufferPtr = ptr\n }\n\n static create(\n width: number,\n height: number,\n widthMethod: WidthMethod,\n options: { respectAlpha?: boolean; id?: string } = {},\n ): OptimizedBuffer {\n const lib = resolveRenderLib()\n const respectAlpha = options.respectAlpha || false\n const id = options.id && options.id.trim() !== \"\" ? options.id : \"unnamed buffer\"\n const buffer = lib.createOptimizedBuffer(width, height, widthMethod, respectAlpha, id)\n return buffer\n }\n\n public get widthMethod(): WidthMethod {\n return this._widthMethod\n }\n\n public get width(): number {\n return this._width\n }\n\n public get height(): number {\n return this._height\n }\n\n public setRespectAlpha(respectAlpha: boolean): void {\n this.guard()\n this.lib.bufferSetRespectAlpha(this.bufferPtr, respectAlpha)\n this.respectAlpha = respectAlpha\n }\n\n public getNativeId(): string {\n this.guard()\n return this.lib.bufferGetId(this.bufferPtr)\n }\n\n public getRealCharBytes(addLineBreaks: boolean = false): Uint8Array {\n this.guard()\n const realSize = this.lib.bufferGetRealCharSize(this.bufferPtr)\n const outputBuffer = new Uint8Array(realSize)\n const bytesWritten = this.lib.bufferWriteResolvedChars(this.bufferPtr, outputBuffer, addLineBreaks)\n return outputBuffer.slice(0, bytesWritten)\n }\n\n public getSpanLines(): CapturedLine[] {\n this.guard()\n const { char, fg, bg, attributes } = this.buffers\n const lines: CapturedLine[] = []\n\n const CHAR_FLAG_CONTINUATION = 0xc0000000 | 0\n const CHAR_FLAG_MASK = 0xc0000000 | 0\n\n const realTextBytes = this.getRealCharBytes(true)\n const realTextLines = new TextDecoder().decode(realTextBytes).split(\"\\n\")\n\n for (let y = 0; y < this._height; y++) {\n const spans: CapturedSpan[] = []\n let currentSpan: CapturedSpan | null = null\n\n const lineChars = [...(realTextLines[y] || \"\")]\n let charIdx = 0\n\n for (let x = 0; x < this._width; x++) {\n const i = y * this._width + x\n const cp = char[i]\n const cellFg = RGBA.fromValues(fg[i * 4], fg[i * 4 + 1], fg[i * 4 + 2], fg[i * 4 + 3])\n const cellBg = RGBA.fromValues(bg[i * 4], bg[i * 4 + 1], bg[i * 4 + 2], bg[i * 4 + 3])\n const cellAttrs = attributes[i] & 0xff\n\n // Continuation cells are placeholders for wide characters (emojis, CJK)\n const isContinuation = (cp & CHAR_FLAG_MASK) === CHAR_FLAG_CONTINUATION\n const cellChar = isContinuation ? \"\" : (lineChars[charIdx++] ?? \" \")\n\n // Check if this cell continues the current span\n if (\n currentSpan &&\n currentSpan.fg.equals(cellFg) &&\n currentSpan.bg.equals(cellBg) &&\n currentSpan.attributes === cellAttrs\n ) {\n currentSpan.text += cellChar\n currentSpan.width += 1\n } else {\n // Start a new span\n if (currentSpan) {\n spans.push(currentSpan)\n }\n currentSpan = {\n text: cellChar,\n fg: cellFg,\n bg: cellBg,\n attributes: cellAttrs,\n width: 1,\n }\n }\n }\n\n // Push the last span\n if (currentSpan) {\n spans.push(currentSpan)\n }\n\n lines.push({ spans })\n }\n\n return lines\n }\n\n public clear(bg: RGBA = RGBA.fromValues(0, 0, 0, 1)): void {\n this.guard()\n this.lib.bufferClear(this.bufferPtr, bg)\n }\n\n public setCell(x: number, y: number, char: string, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n this.guard()\n this.lib.bufferSetCell(this.bufferPtr, x, y, char, fg, bg, attributes)\n }\n\n public setCellWithAlphaBlending(\n x: number,\n y: number,\n char: string,\n fg: RGBA,\n bg: RGBA,\n attributes: number = 0,\n ): void {\n this.guard()\n this.lib.bufferSetCellWithAlphaBlending(this.bufferPtr, x, y, char, fg, bg, attributes)\n }\n\n public drawText(\n text: string,\n x: number,\n y: number,\n fg: RGBA,\n bg?: RGBA,\n attributes: number = 0,\n selection?: { start: number; end: number; bgColor?: RGBA; fgColor?: RGBA } | null,\n ): void {\n this.guard()\n if (!selection) {\n this.lib.bufferDrawText(this.bufferPtr, text, x, y, fg, bg, attributes)\n return\n }\n\n const { start, end } = selection\n\n let selectionBg: RGBA\n let selectionFg: RGBA\n\n if (selection.bgColor) {\n selectionBg = selection.bgColor\n selectionFg = selection.fgColor || fg\n } else {\n const defaultBg = bg || RGBA.fromValues(0, 0, 0, 0)\n selectionFg = defaultBg.a > 0 ? defaultBg : RGBA.fromValues(0, 0, 0, 1)\n selectionBg = fg\n }\n\n if (start > 0) {\n const beforeText = text.slice(0, start)\n this.lib.bufferDrawText(this.bufferPtr, beforeText, x, y, fg, bg, attributes)\n }\n\n if (end > start) {\n const selectedText = text.slice(start, end)\n this.lib.bufferDrawText(this.bufferPtr, selectedText, x + start, y, selectionFg, selectionBg, attributes)\n }\n\n if (end < text.length) {\n const afterText = text.slice(end)\n this.lib.bufferDrawText(this.bufferPtr, afterText, x + end, y, fg, bg, attributes)\n }\n }\n\n public fillRect(x: number, y: number, width: number, height: number, bg: RGBA): void {\n this.lib.bufferFillRect(this.bufferPtr, x, y, width, height, bg)\n }\n\n public colorMatrix(\n matrix: Float32Array,\n cellMask: Float32Array,\n strength: number = 1.0,\n target: TargetChannel = TargetChannel.Both,\n ): void {\n this.guard()\n if (matrix.length !== 16) throw new RangeError(`colorMatrix matrix must have length 16, got ${matrix.length}`)\n const cellMaskCount = Math.floor(cellMask.length / 3)\n this.lib.bufferColorMatrix(this.bufferPtr, ptr(matrix), ptr(cellMask), cellMaskCount, strength, target)\n }\n\n public colorMatrixUniform(\n matrix: Float32Array,\n strength: number = 1.0,\n target: TargetChannel = TargetChannel.Both,\n ): void {\n this.guard()\n if (matrix.length !== 16)\n throw new RangeError(`colorMatrixUniform matrix must have length 16, got ${matrix.length}`)\n if (strength === 0.0) return\n this.lib.bufferColorMatrixUniform(this.bufferPtr, ptr(matrix), strength, target)\n }\n\n public drawFrameBuffer(\n destX: number,\n destY: number,\n frameBuffer: OptimizedBuffer,\n sourceX?: number,\n sourceY?: number,\n sourceWidth?: number,\n sourceHeight?: number,\n ): void {\n this.guard()\n this.lib.drawFrameBuffer(this.bufferPtr, destX, destY, frameBuffer.ptr, sourceX, sourceY, sourceWidth, sourceHeight)\n }\n\n public destroy(): void {\n if (this._destroyed) return\n this._destroyed = true\n this.lib.destroyOptimizedBuffer(this.bufferPtr)\n }\n\n public drawTextBuffer(textBufferView: TextBufferView, x: number, y: number): void {\n this.guard()\n this.lib.bufferDrawTextBufferView(this.bufferPtr, textBufferView.ptr, x, y)\n }\n\n public drawEditorView(editorView: EditorView, x: number, y: number): void {\n this.guard()\n this.lib.bufferDrawEditorView(this.bufferPtr, editorView.ptr, x, y)\n }\n\n public drawSuperSampleBuffer(\n x: number,\n y: number,\n pixelDataPtr: Pointer,\n pixelDataLength: number,\n format: \"bgra8unorm\" | \"rgba8unorm\",\n alignedBytesPerRow: number,\n ): void {\n this.guard()\n this.lib.bufferDrawSuperSampleBuffer(\n this.bufferPtr,\n x,\n y,\n pixelDataPtr,\n pixelDataLength,\n format,\n alignedBytesPerRow,\n )\n }\n\n public drawPackedBuffer(\n dataPtr: Pointer,\n dataLen: number,\n posX: number,\n posY: number,\n terminalWidthCells: number,\n terminalHeightCells: number,\n ): void {\n this.guard()\n this.lib.bufferDrawPackedBuffer(\n this.bufferPtr,\n dataPtr,\n dataLen,\n posX,\n posY,\n terminalWidthCells,\n terminalHeightCells,\n )\n }\n\n public drawGrayscaleBuffer(\n posX: number,\n posY: number,\n intensities: Float32Array,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null = null,\n bg: RGBA | null = null,\n ): void {\n this.guard()\n this.lib.bufferDrawGrayscaleBuffer(this.bufferPtr, posX, posY, ptr(intensities), srcWidth, srcHeight, fg, bg)\n }\n\n public drawGrayscaleBufferSupersampled(\n posX: number,\n posY: number,\n intensities: Float32Array,\n srcWidth: number,\n srcHeight: number,\n fg: RGBA | null = null,\n bg: RGBA | null = null,\n ): void {\n this.guard()\n this.lib.bufferDrawGrayscaleBufferSupersampled(\n this.bufferPtr,\n posX,\n posY,\n ptr(intensities),\n srcWidth,\n srcHeight,\n fg,\n bg,\n )\n }\n\n public resize(width: number, height: number): void {\n this.guard()\n if (this._width === width && this._height === height) return\n\n this._width = width\n this._height = height\n this._rawBuffers = null\n\n this.lib.bufferResize(this.bufferPtr, width, height)\n }\n\n public drawBox(options: {\n x: number\n y: number\n width: number\n height: number\n borderStyle?: BorderStyle\n customBorderChars?: Uint32Array\n border: boolean | BorderSides[]\n borderColor: RGBA\n backgroundColor: RGBA\n shouldFill?: boolean\n title?: string\n titleAlignment?: \"left\" | \"center\" | \"right\"\n }): void {\n this.guard()\n const style = parseBorderStyle(options.borderStyle, \"single\")\n const borderChars: Uint32Array = options.customBorderChars ?? BorderCharArrays[style]\n\n const packedOptions = packDrawOptions(options.border, options.shouldFill ?? false, options.titleAlignment || \"left\")\n\n this.lib.bufferDrawBox(\n this.bufferPtr,\n options.x,\n options.y,\n options.width,\n options.height,\n borderChars,\n packedOptions,\n options.borderColor,\n options.backgroundColor,\n options.title ?? null,\n )\n }\n\n public pushScissorRect(x: number, y: number, width: number, height: number): void {\n this.guard()\n this.lib.bufferPushScissorRect(this.bufferPtr, x, y, width, height)\n }\n\n public popScissorRect(): void {\n this.guard()\n this.lib.bufferPopScissorRect(this.bufferPtr)\n }\n\n public clearScissorRects(): void {\n this.guard()\n this.lib.bufferClearScissorRects(this.bufferPtr)\n }\n\n public pushOpacity(opacity: number): void {\n this.guard()\n this.lib.bufferPushOpacity(this.bufferPtr, Math.max(0, Math.min(1, opacity)))\n }\n\n public popOpacity(): void {\n this.guard()\n this.lib.bufferPopOpacity(this.bufferPtr)\n }\n\n public getCurrentOpacity(): number {\n this.guard()\n return this.lib.bufferGetCurrentOpacity(this.bufferPtr)\n }\n\n public clearOpacity(): void {\n this.guard()\n this.lib.bufferClearOpacity(this.bufferPtr)\n }\n\n public encodeUnicode(text: string): { ptr: Pointer; data: Array<{ width: number; char: number }> } | null {\n this.guard()\n return this.lib.encodeUnicode(text, this._widthMethod)\n }\n\n public freeUnicode(encoded: { ptr: Pointer; data: Array<{ width: number; char: number }> }): void {\n this.guard()\n this.lib.freeUnicode(encoded)\n }\n\n public drawGrid(options: {\n borderChars: Uint32Array\n borderFg: RGBA\n borderBg: RGBA\n columnOffsets: Int32Array\n rowOffsets: Int32Array\n drawInner: boolean\n drawOuter: boolean\n }): void {\n this.guard()\n\n const columnCount = Math.max(0, options.columnOffsets.length - 1)\n const rowCount = Math.max(0, options.rowOffsets.length - 1)\n\n this.lib.bufferDrawGrid(\n this.bufferPtr,\n options.borderChars,\n options.borderFg,\n options.borderBg,\n options.columnOffsets,\n columnCount,\n options.rowOffsets,\n rowCount,\n {\n drawInner: options.drawInner,\n drawOuter: options.drawOuter,\n },\n )\n }\n\n public drawChar(char: number, x: number, y: number, fg: RGBA, bg: RGBA, attributes: number = 0): void {\n this.guard()\n this.lib.bufferDrawChar(this.bufferPtr, char, x, y, fg, bg, attributes)\n }\n}\n",
46
46
  "// src/structs_ffi.ts\nimport { ptr, toArrayBuffer } from \"bun:ffi\";\nfunction fatalError(...args) {\n const message = args.join(\" \");\n console.error(\"FATAL ERROR:\", message);\n throw new Error(message);\n}\nvar pointerSize = process.arch === \"x64\" || process.arch === \"arm64\" ? 8 : 4;\nvar typeSizes = {\n u8: 1,\n bool_u8: 1,\n bool_u32: 4,\n u16: 2,\n i16: 2,\n u32: 4,\n u64: 8,\n f32: 4,\n f64: 8,\n pointer: pointerSize,\n i32: 4\n};\nvar primitiveKeys = Object.keys(typeSizes);\nfunction isPrimitiveType(type) {\n return typeof type === \"string\" && primitiveKeys.includes(type);\n}\nvar typeAlignments = { ...typeSizes };\nvar typeGetters = {\n u8: (view, offset) => view.getUint8(offset),\n bool_u8: (view, offset) => Boolean(view.getUint8(offset)),\n bool_u32: (view, offset) => Boolean(view.getUint32(offset, true)),\n u16: (view, offset) => view.getUint16(offset, true),\n i16: (view, offset) => view.getInt16(offset, true),\n u32: (view, offset) => view.getUint32(offset, true),\n u64: (view, offset) => view.getBigUint64(offset, true),\n f32: (view, offset) => view.getFloat32(offset, true),\n f64: (view, offset) => view.getFloat64(offset, true),\n i32: (view, offset) => view.getInt32(offset, true),\n pointer: (view, offset) => pointerSize === 8 ? view.getBigUint64(offset, true) : BigInt(view.getUint32(offset, true))\n};\nfunction objectPtr() {\n return {\n __type: \"objectPointer\"\n };\n}\nfunction isObjectPointerDef(type) {\n return typeof type === \"object\" && type !== null && type.__type === \"objectPointer\";\n}\nfunction allocStruct(structDef, options) {\n const buffer = new ArrayBuffer(structDef.size);\n const view = new DataView(buffer);\n const result = { buffer, view };\n const { pack: pointerPacker } = primitivePackers(\"pointer\");\n if (options?.lengths) {\n const subBuffers = {};\n for (const [arrayFieldName, length] of Object.entries(options.lengths)) {\n const arrayMeta = structDef.arrayFields.get(arrayFieldName);\n if (!arrayMeta) {\n throw new Error(`Field '${arrayFieldName}' is not an array field with a lengthOf field`);\n }\n const subBuffer = new ArrayBuffer(length * arrayMeta.elementSize);\n subBuffers[arrayFieldName] = subBuffer;\n const pointer = length > 0 ? ptr(subBuffer) : null;\n pointerPacker(view, arrayMeta.arrayOffset, pointer);\n arrayMeta.lengthPack(view, arrayMeta.lengthOffset, length);\n }\n if (Object.keys(subBuffers).length > 0) {\n result.subBuffers = subBuffers;\n }\n }\n return result;\n}\nfunction alignOffset(offset, align) {\n return offset + (align - 1) & ~(align - 1);\n}\nfunction enumTypeError(value) {\n throw new TypeError(`Invalid enum value: ${value}`);\n}\nfunction defineEnum(mapping, base = \"u32\") {\n const reverse = Object.fromEntries(Object.entries(mapping).map(([k, v]) => [v, k]));\n return {\n __type: \"enum\",\n type: base,\n to(value) {\n return typeof value === \"number\" ? value : mapping[value] ?? enumTypeError(String(value));\n },\n from(value) {\n return reverse[value] ?? enumTypeError(String(value));\n },\n enum: mapping\n };\n}\nfunction isEnum(type) {\n return typeof type === \"object\" && type.__type === \"enum\";\n}\nfunction isStruct(type) {\n return typeof type === \"object\" && type.__type === \"struct\";\n}\nfunction primitivePackers(type) {\n let pack;\n let unpack;\n switch (type) {\n case \"u8\":\n pack = (view, off, val) => view.setUint8(off, val);\n unpack = (view, off) => view.getUint8(off);\n break;\n case \"bool_u8\":\n pack = (view, off, val) => view.setUint8(off, val ? 1 : 0);\n unpack = (view, off) => Boolean(view.getUint8(off));\n break;\n case \"bool_u32\":\n pack = (view, off, val) => view.setUint32(off, val ? 1 : 0, true);\n unpack = (view, off) => Boolean(view.getUint32(off, true));\n break;\n case \"u16\":\n pack = (view, off, val) => view.setUint16(off, val, true);\n unpack = (view, off) => view.getUint16(off, true);\n break;\n case \"i16\":\n pack = (view, off, val) => view.setInt16(off, val, true);\n unpack = (view, off) => view.getInt16(off, true);\n break;\n case \"u32\":\n pack = (view, off, val) => view.setUint32(off, val, true);\n unpack = (view, off) => view.getUint32(off, true);\n break;\n case \"i32\":\n pack = (view, off, val) => view.setInt32(off, val, true);\n unpack = (view, off) => view.getInt32(off, true);\n break;\n case \"u64\":\n pack = (view, off, val) => view.setBigUint64(off, BigInt(val), true);\n unpack = (view, off) => view.getBigUint64(off, true);\n break;\n case \"f32\":\n pack = (view, off, val) => view.setFloat32(off, val, true);\n unpack = (view, off) => view.getFloat32(off, true);\n break;\n case \"f64\":\n pack = (view, off, val) => view.setFloat64(off, val, true);\n unpack = (view, off) => view.getFloat64(off, true);\n break;\n case \"pointer\":\n pack = (view, off, val) => {\n pointerSize === 8 ? view.setBigUint64(off, val ? BigInt(val) : 0n, true) : view.setUint32(off, val ? Number(val) : 0, true);\n };\n unpack = (view, off) => {\n const bint = pointerSize === 8 ? view.getBigUint64(off, true) : BigInt(view.getUint32(off, true));\n return Number(bint);\n };\n break;\n default:\n fatalError(`Unsupported primitive type: ${type}`);\n }\n return { pack, unpack };\n}\nvar { pack: pointerPacker, unpack: pointerUnpacker } = primitivePackers(\"pointer\");\nfunction packObjectArray(val) {\n const buffer = new ArrayBuffer(val.length * pointerSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < val.length; i++) {\n const instance = val[i];\n const ptrValue = instance?.ptr ?? null;\n pointerPacker(bufferView, i * pointerSize, ptrValue);\n }\n return bufferView;\n}\nvar encoder = new TextEncoder;\nvar decoder = new TextDecoder;\nfunction defineStruct(fields, structDefOptions) {\n let offset = 0;\n let maxAlign = 1;\n const layout = [];\n const lengthOfFields = {};\n const lengthOfRequested = [];\n const arrayFieldsMetadata = {};\n for (const [name, typeOrStruct, options = {}] of fields) {\n if (options.condition && !options.condition()) {\n continue;\n }\n let size = 0, align = 0;\n let pack;\n let unpack;\n let needsLengthOf = false;\n let lengthOfDef = null;\n if (isPrimitiveType(typeOrStruct)) {\n size = typeSizes[typeOrStruct];\n align = typeAlignments[typeOrStruct];\n ({ pack, unpack } = primitivePackers(typeOrStruct));\n } else if (typeof typeOrStruct === \"string\" && typeOrStruct === \"cstring\") {\n size = pointerSize;\n align = pointerSize;\n pack = (view, off, val) => {\n const bufPtr = val ? ptr(encoder.encode(val + \"\\x00\")) : null;\n pointerPacker(view, off, bufPtr);\n };\n unpack = (view, off) => {\n const ptrVal = pointerUnpacker(view, off);\n return ptrVal;\n };\n } else if (typeof typeOrStruct === \"string\" && typeOrStruct === \"char*\") {\n size = pointerSize;\n align = pointerSize;\n pack = (view, off, val) => {\n const bufPtr = val ? ptr(encoder.encode(val)) : null;\n pointerPacker(view, off, bufPtr);\n };\n unpack = (view, off) => {\n const ptrVal = pointerUnpacker(view, off);\n return ptrVal;\n };\n needsLengthOf = true;\n } else if (isEnum(typeOrStruct)) {\n const base = typeOrStruct.type;\n size = typeSizes[base];\n align = typeAlignments[base];\n const { pack: packEnum } = primitivePackers(base);\n pack = (view, off, val) => {\n const num = typeOrStruct.to(val);\n packEnum(view, off, num);\n };\n unpack = (view, off) => {\n const raw = typeGetters[base](view, off);\n return typeOrStruct.from(raw);\n };\n } else if (isStruct(typeOrStruct)) {\n if (options.asPointer === true) {\n size = pointerSize;\n align = pointerSize;\n pack = (view, off, val, obj, options2) => {\n if (!val) {\n pointerPacker(view, off, null);\n return;\n }\n const nestedBuf = typeOrStruct.pack(val, options2);\n pointerPacker(view, off, ptr(nestedBuf));\n };\n unpack = (view, off) => {\n throw new Error(\"Not implemented yet\");\n };\n } else {\n size = typeOrStruct.size;\n align = typeOrStruct.align;\n pack = (view, off, val, obj, options2) => {\n const nestedBuf = typeOrStruct.pack(val, options2);\n const nestedView = new Uint8Array(nestedBuf);\n const dView = new Uint8Array(view.buffer);\n dView.set(nestedView, off);\n };\n unpack = (view, off) => {\n const slice = view.buffer.slice(off, off + size);\n return typeOrStruct.unpack(slice);\n };\n }\n } else if (isObjectPointerDef(typeOrStruct)) {\n size = pointerSize;\n align = pointerSize;\n pack = (view, off, value) => {\n const ptrValue = value?.ptr ?? null;\n if (ptrValue === undefined) {\n console.warn(`Field '${name}' expected object with '.ptr' property, but got undefined pointer value from:`, value);\n pointerPacker(view, off, null);\n } else {\n pointerPacker(view, off, ptrValue);\n }\n };\n unpack = (view, off) => {\n return pointerUnpacker(view, off);\n };\n } else if (Array.isArray(typeOrStruct) && typeOrStruct.length === 1 && typeOrStruct[0] !== undefined) {\n const [def] = typeOrStruct;\n size = pointerSize;\n align = pointerSize;\n let arrayElementSize;\n if (isEnum(def)) {\n arrayElementSize = typeSizes[def.type];\n pack = (view, off, val, obj) => {\n if (!val || val.length === 0) {\n pointerPacker(view, off, null);\n return;\n }\n const buffer = new ArrayBuffer(val.length * arrayElementSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < val.length; i++) {\n const num = def.to(val[i]);\n bufferView.setUint32(i * arrayElementSize, num, true);\n }\n pointerPacker(view, off, ptr(buffer));\n };\n unpack = null;\n needsLengthOf = true;\n lengthOfDef = def;\n } else if (isStruct(def)) {\n arrayElementSize = def.size;\n pack = (view, off, val, obj, options2) => {\n if (!val || val.length === 0) {\n pointerPacker(view, off, null);\n return;\n }\n const buffer = new ArrayBuffer(val.length * arrayElementSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < val.length; i++) {\n def.packInto(val[i], bufferView, i * arrayElementSize, options2);\n }\n pointerPacker(view, off, ptr(buffer));\n };\n unpack = (view, off) => {\n throw new Error(\"Not implemented yet\");\n };\n } else if (isPrimitiveType(def)) {\n arrayElementSize = typeSizes[def];\n const { pack: primitivePack } = primitivePackers(def);\n pack = (view, off, val) => {\n if (!val || val.length === 0) {\n pointerPacker(view, off, null);\n return;\n }\n const buffer = new ArrayBuffer(val.length * arrayElementSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < val.length; i++) {\n primitivePack(bufferView, i * arrayElementSize, val[i]);\n }\n pointerPacker(view, off, ptr(buffer));\n };\n unpack = null;\n needsLengthOf = true;\n lengthOfDef = def;\n } else if (isObjectPointerDef(def)) {\n arrayElementSize = pointerSize;\n pack = (view, off, val) => {\n if (!val || val.length === 0) {\n pointerPacker(view, off, null);\n return;\n }\n const packedView = packObjectArray(val);\n pointerPacker(view, off, ptr(packedView.buffer));\n };\n unpack = () => {\n throw new Error(\"not implemented yet\");\n };\n } else {\n throw new Error(`Unsupported array element type for ${name}: ${JSON.stringify(def)}`);\n }\n const lengthOfField = Object.values(lengthOfFields).find((f) => f.lengthOf === name);\n if (lengthOfField && isPrimitiveType(lengthOfField.type)) {\n const { pack: lengthPack } = primitivePackers(lengthOfField.type);\n arrayFieldsMetadata[name] = {\n elementSize: arrayElementSize,\n arrayOffset: offset,\n lengthOffset: lengthOfField.offset,\n lengthPack\n };\n }\n } else {\n throw new Error(`Unsupported field type for ${name}: ${JSON.stringify(typeOrStruct)}`);\n }\n offset = alignOffset(offset, align);\n if (options.unpackTransform) {\n const originalUnpack = unpack;\n unpack = (view, off) => options.unpackTransform(originalUnpack(view, off));\n }\n if (options.packTransform) {\n const originalPack = pack;\n pack = (view, off, val, obj, packOptions) => originalPack(view, off, options.packTransform(val), obj, packOptions);\n }\n if (options.optional) {\n const originalPack = pack;\n if (isStruct(typeOrStruct) && !options.asPointer) {\n pack = (view, off, val, obj, packOptions) => {\n if (val || options.mapOptionalInline) {\n originalPack(view, off, val, obj, packOptions);\n }\n };\n } else {\n pack = (view, off, val, obj, packOptions) => originalPack(view, off, val ?? 0, obj, packOptions);\n }\n }\n if (options.lengthOf) {\n const originalPack = pack;\n pack = (view, off, val, obj, packOptions) => {\n const targetValue = obj[options.lengthOf];\n let length = 0;\n if (targetValue) {\n if (typeof targetValue === \"string\") {\n length = Buffer.byteLength(targetValue);\n } else {\n length = targetValue.length;\n }\n }\n return originalPack(view, off, length, obj, packOptions);\n };\n }\n let validateFunctions;\n if (options.validate) {\n validateFunctions = Array.isArray(options.validate) ? options.validate : [options.validate];\n }\n const layoutField = {\n name,\n offset,\n size,\n align,\n validate: validateFunctions,\n optional: !!options.optional || !!options.lengthOf || options.default !== undefined,\n default: options.default,\n pack,\n unpack,\n type: typeOrStruct,\n lengthOf: options.lengthOf\n };\n layout.push(layoutField);\n if (options.lengthOf) {\n lengthOfFields[options.lengthOf] = layoutField;\n }\n if (needsLengthOf) {\n const def = typeof typeOrStruct === \"string\" && typeOrStruct === \"char*\" ? \"char*\" : lengthOfDef;\n if (!def)\n fatalError(`Internal error: needsLengthOf=true but def is null for ${name}`);\n lengthOfRequested.push({ requester: layoutField, def });\n }\n offset += size;\n maxAlign = Math.max(maxAlign, align);\n }\n for (const { requester, def } of lengthOfRequested) {\n const lengthOfField = lengthOfFields[requester.name];\n if (!lengthOfField) {\n if (def === \"char*\") {\n continue;\n }\n throw new Error(`lengthOf field not found for array field ${requester.name}`);\n }\n if (def === \"char*\") {\n requester.unpack = (view, off) => {\n const ptrAddress = pointerUnpacker(view, off);\n const length = lengthOfField.unpack(view, lengthOfField.offset);\n if (ptrAddress === 0) {\n return null;\n }\n const byteLength = typeof length === \"bigint\" ? Number(length) : length;\n if (byteLength === 0) {\n return \"\";\n }\n const buffer = toArrayBuffer(ptrAddress, 0, byteLength);\n return decoder.decode(buffer);\n };\n } else if (isPrimitiveType(def)) {\n const elemSize = typeSizes[def];\n const { unpack: primitiveUnpack } = primitivePackers(def);\n requester.unpack = (view, off) => {\n const result = [];\n const length = lengthOfField.unpack(view, lengthOfField.offset);\n const ptrAddress = pointerUnpacker(view, off);\n if (ptrAddress === 0n && length > 0) {\n throw new Error(`Array field ${requester.name} has null pointer but length ${length}.`);\n }\n if (ptrAddress === 0n || length === 0) {\n return [];\n }\n const buffer = toArrayBuffer(ptrAddress, 0, length * elemSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < length; i++) {\n result.push(primitiveUnpack(bufferView, i * elemSize));\n }\n return result;\n };\n } else {\n const elemSize = def.type === \"u32\" ? 4 : 8;\n requester.unpack = (view, off) => {\n const result = [];\n const length = lengthOfField.unpack(view, lengthOfField.offset);\n const ptrAddress = pointerUnpacker(view, off);\n if (ptrAddress === 0n && length > 0) {\n throw new Error(`Array field ${requester.name} has null pointer but length ${length}.`);\n }\n if (ptrAddress === 0n || length === 0) {\n return [];\n }\n const buffer = toArrayBuffer(ptrAddress, 0, length * elemSize);\n const bufferView = new DataView(buffer);\n for (let i = 0;i < length; i++) {\n result.push(def.from(bufferView.getUint32(i * elemSize, true)));\n }\n return result;\n };\n }\n }\n const totalSize = alignOffset(offset, maxAlign);\n const description = layout.map((f) => ({\n name: f.name,\n offset: f.offset,\n size: f.size,\n align: f.align,\n optional: f.optional,\n type: f.type,\n lengthOf: f.lengthOf\n }));\n const layoutByName = new Map(description.map((f) => [f.name, f]));\n const arrayFields = new Map(Object.entries(arrayFieldsMetadata));\n return {\n __type: \"struct\",\n size: totalSize,\n align: maxAlign,\n hasMapValue: !!structDefOptions?.mapValue,\n layoutByName,\n arrayFields,\n pack(obj, options) {\n const buf = new ArrayBuffer(totalSize);\n const view = new DataView(buf);\n let mappedObj = obj;\n if (structDefOptions?.mapValue) {\n mappedObj = structDefOptions.mapValue(obj);\n }\n for (const field of layout) {\n const value = mappedObj[field.name] ?? field.default;\n if (!field.optional && value === undefined) {\n fatalError(`Packing non-optional field '${field.name}' but value is undefined (and no default provided)`);\n }\n if (field.validate) {\n for (const validateFn of field.validate) {\n validateFn(value, field.name, {\n hints: options?.validationHints,\n input: mappedObj\n });\n }\n }\n field.pack(view, field.offset, value, mappedObj, options);\n }\n return view.buffer;\n },\n packInto(obj, view, offset2, options) {\n let mappedObj = obj;\n if (structDefOptions?.mapValue) {\n mappedObj = structDefOptions.mapValue(obj);\n }\n for (const field of layout) {\n const value = mappedObj[field.name] ?? field.default;\n if (!field.optional && value === undefined) {\n console.warn(`packInto missing value for non-optional field '${field.name}' at offset ${offset2 + field.offset}. Writing default or zero.`);\n }\n if (field.validate) {\n for (const validateFn of field.validate) {\n validateFn(value, field.name, {\n hints: options?.validationHints,\n input: mappedObj\n });\n }\n }\n field.pack(view, offset2 + field.offset, value, mappedObj, options);\n }\n },\n unpack(buf) {\n if (buf.byteLength < totalSize) {\n fatalError(`Buffer size (${buf.byteLength}) is smaller than struct size (${totalSize}) for unpacking.`);\n }\n const view = new DataView(buf);\n const result = structDefOptions?.default ? { ...structDefOptions.default } : {};\n for (const field of layout) {\n if (!field.unpack) {\n continue;\n }\n try {\n result[field.name] = field.unpack(view, field.offset);\n } catch (e) {\n console.error(`Error unpacking field '${field.name}' at offset ${field.offset}:`, e);\n throw e;\n }\n }\n if (structDefOptions?.reduceValue) {\n return structDefOptions.reduceValue(result);\n }\n return result;\n },\n packList(objects, options) {\n if (objects.length === 0) {\n return new ArrayBuffer(0);\n }\n const buffer = new ArrayBuffer(totalSize * objects.length);\n const view = new DataView(buffer);\n for (let i = 0;i < objects.length; i++) {\n let mappedObj = objects[i];\n if (structDefOptions?.mapValue) {\n mappedObj = structDefOptions.mapValue(objects[i]);\n }\n for (const field of layout) {\n const value = mappedObj[field.name] ?? field.default;\n if (!field.optional && value === undefined) {\n fatalError(`Packing non-optional field '${field.name}' at index ${i} but value is undefined (and no default provided)`);\n }\n if (field.validate) {\n for (const validateFn of field.validate) {\n validateFn(value, field.name, {\n hints: options?.validationHints,\n input: mappedObj\n });\n }\n }\n field.pack(view, i * totalSize + field.offset, value, mappedObj, options);\n }\n }\n return buffer;\n },\n unpackList(buf, count) {\n if (count === 0) {\n return [];\n }\n const expectedSize = totalSize * count;\n if (buf.byteLength < expectedSize) {\n fatalError(`Buffer size (${buf.byteLength}) is smaller than expected size (${expectedSize}) for unpacking ${count} structs.`);\n }\n const view = new DataView(buf);\n const results = [];\n for (let i = 0;i < count; i++) {\n const offset2 = i * totalSize;\n const result = structDefOptions?.default ? { ...structDefOptions.default } : {};\n for (const field of layout) {\n if (!field.unpack) {\n continue;\n }\n try {\n result[field.name] = field.unpack(view, offset2 + field.offset);\n } catch (e) {\n console.error(`Error unpacking field '${field.name}' at index ${i}, offset ${offset2 + field.offset}:`, e);\n throw e;\n }\n }\n if (structDefOptions?.reduceValue) {\n results.push(structDefOptions.reduceValue(result));\n } else {\n results.push(result);\n }\n }\n return results;\n },\n describe() {\n return description;\n }\n };\n}\nexport {\n pointerSize,\n packObjectArray,\n objectPtr,\n defineStruct,\n defineEnum,\n allocStruct\n};\n",
47
47
  "import { defineStruct, defineEnum } from \"bun-ffi-structs\"\nimport { ptr, toArrayBuffer, type Pointer } from \"bun:ffi\"\nimport { RGBA } from \"./lib/RGBA.js\"\n\nconst rgbaPackTransform = (rgba?: RGBA) => (rgba ? ptr(rgba.buffer) : null)\nconst rgbaUnpackTransform = (ptr?: Pointer) => (ptr ? RGBA.fromArray(new Float32Array(toArrayBuffer(ptr))) : undefined)\n\ntype StyledChunkInput = {\n text: string\n fg?: RGBA | null\n bg?: RGBA | null\n attributes?: number | null\n link?: { url: string } | string | null\n}\n\nexport const StyledChunkStruct = defineStruct(\n [\n [\"text\", \"char*\"],\n [\"text_len\", \"u64\", { lengthOf: \"text\" }],\n [\n \"fg\",\n \"pointer\",\n {\n optional: true,\n packTransform: rgbaPackTransform,\n unpackTransform: rgbaUnpackTransform,\n },\n ],\n [\n \"bg\",\n \"pointer\",\n {\n optional: true,\n packTransform: rgbaPackTransform,\n unpackTransform: rgbaUnpackTransform,\n },\n ],\n [\"attributes\", \"u32\", { default: 0 }],\n [\"link\", \"char*\", { default: \"\" }],\n [\"link_len\", \"u64\", { lengthOf: \"link\" }],\n ],\n {\n mapValue: (chunk: StyledChunkInput): StyledChunkInput => {\n if (!chunk.link || typeof chunk.link === \"string\") {\n return chunk\n }\n\n return {\n ...chunk,\n link: chunk.link.url,\n }\n },\n },\n)\n\nexport const HighlightStruct = defineStruct([\n [\"start\", \"u32\"],\n [\"end\", \"u32\"],\n [\"styleId\", \"u32\"],\n [\"priority\", \"u8\", { default: 0 }],\n [\"hlRef\", \"u16\", { default: 0 }],\n])\n\nexport const LogicalCursorStruct = defineStruct([\n [\"row\", \"u32\"],\n [\"col\", \"u32\"],\n [\"offset\", \"u32\"],\n])\n\nexport const VisualCursorStruct = defineStruct([\n [\"visualRow\", \"u32\"],\n [\"visualCol\", \"u32\"],\n [\"logicalRow\", \"u32\"],\n [\"logicalCol\", \"u32\"],\n [\"offset\", \"u32\"],\n])\n\nconst UnicodeMethodEnum = defineEnum({ wcwidth: 0, unicode: 1 }, \"u8\")\n\nexport const TerminalCapabilitiesStruct = defineStruct([\n [\"kitty_keyboard\", \"bool_u8\"],\n [\"kitty_graphics\", \"bool_u8\"],\n [\"rgb\", \"bool_u8\"],\n [\"unicode\", UnicodeMethodEnum],\n [\"sgr_pixels\", \"bool_u8\"],\n [\"color_scheme_updates\", \"bool_u8\"],\n [\"explicit_width\", \"bool_u8\"],\n [\"scaled_text\", \"bool_u8\"],\n [\"sixel\", \"bool_u8\"],\n [\"focus_tracking\", \"bool_u8\"],\n [\"sync\", \"bool_u8\"],\n [\"bracketed_paste\", \"bool_u8\"],\n [\"hyperlinks\", \"bool_u8\"],\n [\"osc52\", \"bool_u8\"],\n [\"explicit_cursor_positioning\", \"bool_u8\"],\n [\"term_name\", \"char*\"],\n [\"term_name_len\", \"u64\", { lengthOf: \"term_name\" }],\n [\"term_version\", \"char*\"],\n [\"term_version_len\", \"u64\", { lengthOf: \"term_version\" }],\n [\"term_from_xtversion\", \"bool_u8\"],\n])\n\nexport const EncodedCharStruct = defineStruct([\n [\"width\", \"u8\"],\n [\"char\", \"u32\"],\n])\n\nexport const LineInfoStruct = defineStruct([\n [\"startCols\", [\"u32\"]],\n [\"startColsLen\", \"u32\", { lengthOf: \"startCols\" }],\n [\"widthCols\", [\"u32\"]],\n [\"widthColsLen\", \"u32\", { lengthOf: \"widthCols\" }],\n [\"sources\", [\"u32\"]],\n [\"sourcesLen\", \"u32\", { lengthOf: \"sources\" }],\n [\"wraps\", [\"u32\"]],\n [\"wrapsLen\", \"u32\", { lengthOf: \"wraps\" }],\n [\"widthColsMax\", \"u32\"],\n])\n\nexport const MeasureResultStruct = defineStruct([\n [\"lineCount\", \"u32\"],\n [\"widthColsMax\", \"u32\"],\n])\n\nexport const CursorStateStruct = defineStruct([\n [\"x\", \"u32\"],\n [\"y\", \"u32\"],\n [\"visible\", \"bool_u8\"],\n [\"style\", \"u8\"],\n [\"blinking\", \"bool_u8\"],\n [\"r\", \"f32\"],\n [\"g\", \"f32\"],\n [\"b\", \"f32\"],\n [\"a\", \"f32\"],\n])\n\nexport const CursorStyleOptionsStruct = defineStruct([\n [\"style\", \"u8\", { default: 255 }],\n [\"blinking\", \"u8\", { default: 255 }],\n [\n \"color\",\n \"pointer\",\n {\n optional: true,\n packTransform: rgbaPackTransform,\n unpackTransform: rgbaUnpackTransform,\n },\n ],\n [\"cursor\", \"u8\", { default: 255 }],\n])\n\nexport const GridDrawOptionsStruct = defineStruct([\n [\"drawInner\", \"bool_u8\", { default: true }],\n [\"drawOuter\", \"bool_u8\", { default: true }],\n])\n\nexport type BuildOptions = {\n gpaSafeStats: boolean\n gpaMemoryLimitTracking: boolean\n}\n\nexport const BuildOptionsStruct = defineStruct([\n [\"gpaSafeStats\", \"bool_u8\"],\n [\"gpaMemoryLimitTracking\", \"bool_u8\"],\n])\n\nexport type AllocatorStats = {\n totalRequestedBytes: number\n activeAllocations: number\n smallAllocations: number\n largeAllocations: number\n requestedBytesValid: boolean\n}\n\nexport const AllocatorStatsStruct = defineStruct([\n [\"totalRequestedBytes\", \"u64\"],\n [\"activeAllocations\", \"u64\"],\n [\"smallAllocations\", \"u64\"],\n [\"largeAllocations\", \"u64\"],\n [\"requestedBytesValid\", \"bool_u8\"],\n])\n\nexport type GrowthPolicy = \"grow\" | \"block\"\n\nexport type NativeSpanFeedOptions = {\n chunkSize?: number\n initialChunks?: number\n maxBytes?: bigint\n growthPolicy?: GrowthPolicy\n autoCommitOnFull?: boolean\n spanQueueCapacity?: number\n}\n\nexport type NativeSpanFeedStats = {\n bytesWritten: bigint\n spansCommitted: bigint\n chunks: number\n pendingSpans: number\n}\n\nexport type SpanInfo = {\n chunkPtr: Pointer\n offset: number\n len: number\n chunkIndex: number\n}\n\nexport type ReserveInfo = {\n ptr: Pointer\n len: number\n}\n\nconst GrowthPolicyEnum = defineEnum({ grow: 0, block: 1 }, \"u8\")\n\nexport const NativeSpanFeedOptionsStruct = defineStruct([\n [\"chunkSize\", \"u32\", { default: 64 * 1024 }],\n [\"initialChunks\", \"u32\", { default: 2 }],\n [\"maxBytes\", \"u64\", { default: 0n }],\n [\"growthPolicy\", GrowthPolicyEnum, { default: \"grow\" }],\n [\"autoCommitOnFull\", \"bool_u8\", { default: true }],\n [\"spanQueueCapacity\", \"u32\", { default: 0 }],\n])\n\nexport const NativeSpanFeedStatsStruct = defineStruct([\n [\"bytesWritten\", \"u64\"],\n [\"spansCommitted\", \"u64\"],\n [\"chunks\", \"u32\"],\n [\"pendingSpans\", \"u32\"],\n])\n\nexport const SpanInfoStruct = defineStruct(\n [\n [\"chunkPtr\", \"pointer\"],\n [\"offset\", \"u32\"],\n [\"len\", \"u32\"],\n [\"chunkIndex\", \"u32\"],\n [\"reserved\", \"u32\", { default: 0 }],\n ],\n {\n reduceValue: (value: { chunkPtr: Pointer; offset: number; len: number; chunkIndex: number }) => ({\n chunkPtr: value.chunkPtr as Pointer,\n offset: value.offset,\n len: value.len,\n chunkIndex: value.chunkIndex,\n }),\n },\n)\n\nexport const ReserveInfoStruct = defineStruct(\n [\n [\"ptr\", \"pointer\"],\n [\"len\", \"u32\"],\n [\"reserved\", \"u32\", { default: 0 }],\n ],\n {\n reduceValue: (value: { ptr: Pointer; len: number }) => ({\n ptr: value.ptr as Pointer,\n len: value.len,\n }),\n },\n)\n",
48
48
  "import type { StyledText } from \"./lib/styled-text.js\"\nimport { RGBA } from \"./lib/RGBA.js\"\nimport { resolveRenderLib, type LineInfo, type RenderLib } from \"./zig.js\"\nimport { type Pointer } from \"bun:ffi\"\nimport { type WidthMethod, type Highlight } from \"./types.js\"\nimport type { SyntaxStyle } from \"./syntax-style.js\"\n\nexport interface TextChunk {\n __isChunk: true\n text: string\n fg?: RGBA\n bg?: RGBA\n attributes?: number\n link?: { url: string }\n}\n\nexport class TextBuffer {\n private lib: RenderLib\n private bufferPtr: Pointer\n private _length: number = 0\n private _byteSize: number = 0\n private _lineInfo?: LineInfo\n private _destroyed: boolean = false\n private _syntaxStyle?: SyntaxStyle\n private _textBytes?: Uint8Array\n private _memId?: number\n private _appendedChunks: Uint8Array[] = []\n\n constructor(lib: RenderLib, ptr: Pointer) {\n this.lib = lib\n this.bufferPtr = ptr\n }\n\n static create(widthMethod: WidthMethod): TextBuffer {\n const lib = resolveRenderLib()\n return lib.createTextBuffer(widthMethod)\n }\n\n // Fail loud and clear\n // Instead of trying to return values that could work or not,\n // this at least will show a stack trace to know where the call to a destroyed TextBuffer was made\n private guard(): void {\n if (this._destroyed) throw new Error(\"TextBuffer is destroyed\")\n }\n\n public setText(text: string): void {\n this.guard()\n this._textBytes = this.lib.encoder.encode(text)\n\n if (this._memId === undefined) {\n this._memId = this.lib.textBufferRegisterMemBuffer(this.bufferPtr, this._textBytes, false)\n } else {\n this.lib.textBufferReplaceMemBuffer(this.bufferPtr, this._memId, this._textBytes, false)\n }\n\n this.lib.textBufferSetTextFromMem(this.bufferPtr, this._memId)\n this._length = this.lib.textBufferGetLength(this.bufferPtr)\n this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n this._lineInfo = undefined\n this._appendedChunks = [] // Clear any previously appended chunks\n }\n\n public append(text: string): void {\n this.guard()\n const textBytes = this.lib.encoder.encode(text)\n // Keep the bytes alive to prevent garbage collection\n this._appendedChunks.push(textBytes)\n this.lib.textBufferAppend(this.bufferPtr, textBytes)\n this._length = this.lib.textBufferGetLength(this.bufferPtr)\n this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n this._lineInfo = undefined\n }\n\n public loadFile(path: string): void {\n this.guard()\n const success = this.lib.textBufferLoadFile(this.bufferPtr, path)\n if (!success) {\n throw new Error(`Failed to load file: ${path}`)\n }\n this._length = this.lib.textBufferGetLength(this.bufferPtr)\n this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n this._lineInfo = undefined\n this._textBytes = undefined\n }\n\n public setStyledText(text: StyledText): void {\n this.guard()\n\n this.lib.textBufferSetStyledText(this.bufferPtr, text.chunks)\n\n this._length = this.lib.textBufferGetLength(this.bufferPtr)\n this._byteSize = this.lib.textBufferGetByteSize(this.bufferPtr)\n this._lineInfo = undefined\n }\n\n public setDefaultFg(fg: RGBA | null): void {\n this.guard()\n this.lib.textBufferSetDefaultFg(this.bufferPtr, fg)\n }\n\n public setDefaultBg(bg: RGBA | null): void {\n this.guard()\n this.lib.textBufferSetDefaultBg(this.bufferPtr, bg)\n }\n\n public setDefaultAttributes(attributes: number | null): void {\n this.guard()\n this.lib.textBufferSetDefaultAttributes(this.bufferPtr, attributes)\n }\n\n public resetDefaults(): void {\n this.guard()\n this.lib.textBufferResetDefaults(this.bufferPtr)\n }\n\n public getLineCount(): number {\n this.guard()\n return this.lib.textBufferGetLineCount(this.bufferPtr)\n }\n\n public get length(): number {\n this.guard()\n return this._length\n }\n\n public get byteSize(): number {\n this.guard()\n return this._byteSize\n }\n\n public get ptr(): Pointer {\n this.guard()\n return this.bufferPtr\n }\n\n public getPlainText(): string {\n this.guard()\n if (this._byteSize === 0) return \"\"\n // Use byteSize for accurate buffer allocation (includes newlines in byte count)\n const plainBytes = this.lib.getPlainTextBytes(this.bufferPtr, this._byteSize)\n\n if (!plainBytes) return \"\"\n\n return this.lib.decoder.decode(plainBytes)\n }\n\n public getTextRange(startOffset: number, endOffset: number): string {\n this.guard()\n if (startOffset >= endOffset) return \"\"\n if (this._byteSize === 0) return \"\"\n\n const rangeBytes = this.lib.textBufferGetTextRange(this.bufferPtr, startOffset, endOffset, this._byteSize)\n\n if (!rangeBytes) return \"\"\n\n return this.lib.decoder.decode(rangeBytes)\n }\n\n /**\n * Add a highlight using character offsets into the full text.\n * start/end in highlight represent absolute character positions.\n */\n public addHighlightByCharRange(highlight: Highlight): void {\n this.guard()\n this.lib.textBufferAddHighlightByCharRange(this.bufferPtr, highlight)\n }\n\n /**\n * Add a highlight to a specific line by column positions.\n * start/end in highlight represent column offsets.\n */\n public addHighlight(lineIdx: number, highlight: Highlight): void {\n this.guard()\n this.lib.textBufferAddHighlight(this.bufferPtr, lineIdx, highlight)\n }\n\n public removeHighlightsByRef(hlRef: number): void {\n this.guard()\n this.lib.textBufferRemoveHighlightsByRef(this.bufferPtr, hlRef)\n }\n\n public clearLineHighlights(lineIdx: number): void {\n this.guard()\n this.lib.textBufferClearLineHighlights(this.bufferPtr, lineIdx)\n }\n\n public clearAllHighlights(): void {\n this.guard()\n this.lib.textBufferClearAllHighlights(this.bufferPtr)\n }\n\n public getLineHighlights(lineIdx: number): Array<Highlight> {\n this.guard()\n return this.lib.textBufferGetLineHighlights(this.bufferPtr, lineIdx)\n }\n\n public getHighlightCount(): number {\n this.guard()\n return this.lib.textBufferGetHighlightCount(this.bufferPtr)\n }\n\n public setSyntaxStyle(style: SyntaxStyle | null): void {\n this.guard()\n this._syntaxStyle = style ?? undefined\n this.lib.textBufferSetSyntaxStyle(this.bufferPtr, style?.ptr ?? null)\n }\n\n public getSyntaxStyle(): SyntaxStyle | null {\n this.guard()\n return this._syntaxStyle ?? null\n }\n\n public setTabWidth(width: number): void {\n this.guard()\n this.lib.textBufferSetTabWidth(this.bufferPtr, width)\n }\n\n public getTabWidth(): number {\n this.guard()\n return this.lib.textBufferGetTabWidth(this.bufferPtr)\n }\n\n public clear(): void {\n this.guard()\n this.lib.textBufferClear(this.bufferPtr)\n this._length = 0\n this._byteSize = 0\n this._lineInfo = undefined\n this._textBytes = undefined\n this._appendedChunks = []\n // Note: _memId is NOT cleared - it can be reused for next setText\n }\n\n public reset(): void {\n this.guard()\n this.lib.textBufferReset(this.bufferPtr)\n this._length = 0\n this._byteSize = 0\n this._lineInfo = undefined\n this._textBytes = undefined\n this._memId = undefined // Reset clears the registry, so clear our ID\n this._appendedChunks = []\n }\n\n public destroy(): void {\n if (this._destroyed) return\n this._destroyed = true\n this.lib.destroyTextBuffer(this.bufferPtr)\n }\n}\n",