@opentui/core 0.0.0-20250922-6d7f4921 → 0.0.0-20250922-2a20774e

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.
package/index.js CHANGED
@@ -111,7 +111,7 @@ import {
111
111
  white,
112
112
  wrapWithDelegates,
113
113
  yellow
114
- } from "./index-ra8j4k81.js";
114
+ } from "./index-mtc20a8y.js";
115
115
  // src/post/filters.ts
116
116
  function applyScanlines(buffer, strength = 0.8, step = 2) {
117
117
  const width = buffer.width;
package/index.js.map CHANGED
@@ -7,7 +7,7 @@
7
7
  "import { Edge, Gutter } from \"yoga-layout\"\nimport { type RenderableOptions, Renderable } from \"../Renderable\"\nimport { isValidPercentage } from \"../lib/renderable.validations\"\nimport type { OptimizedBuffer } from \"../buffer\"\nimport {\n type BorderCharacters,\n type BorderSides,\n type BorderSidesConfig,\n type BorderStyle,\n borderCharsToArray,\n getBorderSides,\n} from \"../lib\"\nimport { type ColorInput, RGBA, parseColor } from \"../lib/RGBA\"\nimport type { RenderContext } from \"../types\"\n\nexport interface BoxOptions<TRenderable extends Renderable = BoxRenderable> extends RenderableOptions<TRenderable> {\n backgroundColor?: string | RGBA\n borderStyle?: BorderStyle\n border?: boolean | BorderSides[]\n borderColor?: string | RGBA\n customBorderChars?: BorderCharacters\n shouldFill?: boolean\n title?: string\n titleAlignment?: \"left\" | \"center\" | \"right\"\n focusedBorderColor?: ColorInput\n gap?: number | `${number}%`\n rowGap?: number | `${number}%`\n columnGap?: number | `${number}%`\n}\n\nfunction isGapType(value: any): value is number | undefined {\n if (value === undefined) {\n return true\n }\n if (typeof value === \"number\" && !Number.isNaN(value)) {\n return true\n }\n return isValidPercentage(value)\n}\n\nexport class BoxRenderable extends Renderable {\n protected _backgroundColor: RGBA\n protected _border: boolean | BorderSides[]\n protected _borderStyle: BorderStyle\n protected _borderColor: RGBA\n protected _focusedBorderColor: RGBA\n private _customBorderCharsObj: BorderCharacters | undefined\n protected _customBorderChars?: Uint32Array\n protected borderSides: BorderSidesConfig\n public shouldFill: boolean\n protected _title?: string\n protected _titleAlignment: \"left\" | \"center\" | \"right\"\n\n protected _defaultOptions = {\n backgroundColor: \"transparent\",\n borderStyle: \"single\",\n border: false,\n borderColor: \"#FFFFFF\",\n shouldFill: true,\n titleAlignment: \"left\",\n focusedBorderColor: \"#00AAFF\",\n } satisfies Partial<BoxOptions>\n\n constructor(ctx: RenderContext, options: BoxOptions) {\n super(ctx, options)\n\n this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor)\n this._border = options.border ?? this._defaultOptions.border\n if (\n !options.border &&\n (options.borderStyle || options.borderColor || options.focusedBorderColor || options.customBorderChars)\n ) {\n this._border = true\n }\n this._borderStyle = options.borderStyle || this._defaultOptions.borderStyle\n this._borderColor = parseColor(options.borderColor || this._defaultOptions.borderColor)\n this._focusedBorderColor = parseColor(options.focusedBorderColor || this._defaultOptions.focusedBorderColor)\n this._customBorderCharsObj = options.customBorderChars\n this._customBorderChars = this._customBorderCharsObj ? borderCharsToArray(this._customBorderCharsObj) : undefined\n this.borderSides = getBorderSides(this._border)\n this.shouldFill = options.shouldFill ?? this._defaultOptions.shouldFill\n this._title = options.title\n this._titleAlignment = options.titleAlignment || this._defaultOptions.titleAlignment\n\n this.applyYogaBorders()\n\n const hasInitialGapProps =\n options.gap !== undefined || options.rowGap !== undefined || options.columnGap !== undefined\n if (hasInitialGapProps) {\n this.applyYogaGap(options)\n }\n }\n\n public get customBorderChars(): BorderCharacters | undefined {\n return this._customBorderCharsObj\n }\n\n public set customBorderChars(value: BorderCharacters | undefined) {\n this._customBorderCharsObj = value\n this._customBorderChars = value ? borderCharsToArray(value) : undefined\n this.requestRender()\n }\n\n public get backgroundColor(): RGBA {\n return this._backgroundColor\n }\n\n public set backgroundColor(value: RGBA | string | undefined) {\n const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n if (this._backgroundColor !== newColor) {\n this._backgroundColor = newColor\n this.requestRender()\n }\n }\n\n public get border(): boolean | BorderSides[] {\n return this._border\n }\n\n public set border(value: boolean | BorderSides[]) {\n if (this._border !== value) {\n this._border = value\n this.borderSides = getBorderSides(value)\n this.applyYogaBorders()\n this.requestRender()\n }\n }\n\n public get borderStyle(): BorderStyle {\n return this._borderStyle\n }\n\n public set borderStyle(value: BorderStyle) {\n let _value = value ?? this._defaultOptions.borderStyle\n if (this._borderStyle !== _value) {\n this._borderStyle = _value\n this._customBorderChars = undefined\n this.requestRender()\n }\n }\n\n public get borderColor(): RGBA {\n return this._borderColor\n }\n\n public set borderColor(value: RGBA | string) {\n const newColor = parseColor(value ?? this._defaultOptions.borderColor)\n if (this._borderColor !== newColor) {\n this._borderColor = newColor\n this.requestRender()\n }\n }\n\n public get focusedBorderColor(): RGBA {\n return this._focusedBorderColor\n }\n\n public set focusedBorderColor(value: RGBA | string) {\n const newColor = parseColor(value ?? this._defaultOptions.focusedBorderColor)\n if (this._focusedBorderColor !== newColor) {\n this._focusedBorderColor = newColor\n if (this._focused) {\n this.requestRender()\n }\n }\n }\n\n public get title(): string | undefined {\n return this._title\n }\n\n public set title(value: string | undefined) {\n if (this._title !== value) {\n this._title = value\n this.requestRender()\n }\n }\n\n public get titleAlignment(): \"left\" | \"center\" | \"right\" {\n return this._titleAlignment\n }\n\n public set titleAlignment(value: \"left\" | \"center\" | \"right\") {\n if (this._titleAlignment !== value) {\n this._titleAlignment = value\n this.requestRender()\n }\n }\n\n protected renderSelf(buffer: OptimizedBuffer): void {\n const currentBorderColor = this._focused ? this._focusedBorderColor : this._borderColor\n\n buffer.drawBox({\n x: this.x,\n y: this.y,\n width: this.width,\n height: this.height,\n borderStyle: this._borderStyle,\n customBorderChars: this._customBorderChars,\n border: this._border,\n borderColor: currentBorderColor,\n backgroundColor: this._backgroundColor,\n shouldFill: this.shouldFill,\n title: this._title,\n titleAlignment: this._titleAlignment,\n })\n }\n\n protected getScissorRect(): { x: number; y: number; width: number; height: number } {\n const baseRect = super.getScissorRect()\n\n if (!this.borderSides.top && !this.borderSides.right && !this.borderSides.bottom && !this.borderSides.left) {\n return baseRect\n }\n\n const leftInset = this.borderSides.left ? 1 : 0\n const rightInset = this.borderSides.right ? 1 : 0\n const topInset = this.borderSides.top ? 1 : 0\n const bottomInset = this.borderSides.bottom ? 1 : 0\n\n return {\n x: baseRect.x + leftInset,\n y: baseRect.y + topInset,\n width: Math.max(0, baseRect.width - leftInset - rightInset),\n height: Math.max(0, baseRect.height - topInset - bottomInset),\n }\n }\n\n private applyYogaBorders(): void {\n const node = this.yogaNode\n node.setBorder(Edge.Left, this.borderSides.left ? 1 : 0)\n node.setBorder(Edge.Right, this.borderSides.right ? 1 : 0)\n node.setBorder(Edge.Top, this.borderSides.top ? 1 : 0)\n node.setBorder(Edge.Bottom, this.borderSides.bottom ? 1 : 0)\n this.requestRender()\n }\n\n private applyYogaGap(options: BoxOptions): void {\n const node = this.yogaNode\n\n if (isGapType(options.gap)) {\n node.setGap(Gutter.All, options.gap)\n }\n\n if (isGapType(options.rowGap)) {\n node.setGap(Gutter.Row, options.rowGap)\n }\n\n if (isGapType(options.columnGap)) {\n node.setGap(Gutter.Column, options.columnGap)\n }\n }\n\n public set gap(gap: number | `${number}%` | undefined) {\n if (isGapType(gap)) {\n this.yogaNode.setGap(Gutter.All, gap)\n this.requestRender()\n }\n }\n\n public set rowGap(rowGap: number | `${number}%` | undefined) {\n if (isGapType(rowGap)) {\n this.yogaNode.setGap(Gutter.Row, rowGap)\n this.requestRender()\n }\n }\n\n public set columnGap(columnGap: number | `${number}%` | undefined) {\n if (isGapType(columnGap)) {\n this.yogaNode.setGap(Gutter.Column, columnGap)\n this.requestRender()\n }\n }\n}\n",
8
8
  "import { type RenderableOptions, Renderable } from \"../Renderable\"\nimport { OptimizedBuffer } from \"../buffer\"\nimport type { RenderContext } from \"../types\"\n\nexport interface FrameBufferOptions extends RenderableOptions<FrameBufferRenderable> {\n width: number\n height: number\n respectAlpha?: boolean\n}\n\nexport class FrameBufferRenderable extends Renderable {\n public frameBuffer: OptimizedBuffer\n protected respectAlpha: boolean\n\n constructor(ctx: RenderContext, options: FrameBufferOptions) {\n super(ctx, options)\n this.respectAlpha = options.respectAlpha || false\n this.frameBuffer = OptimizedBuffer.create(options.width, options.height, this._ctx.widthMethod, {\n respectAlpha: this.respectAlpha,\n id: options.id || `framebufferrenderable-${this.id}`,\n })\n }\n\n protected onResize(width: number, height: number): void {\n if (width <= 0 || height <= 0) {\n throw new Error(`Invalid resize dimensions for FrameBufferRenderable ${this.id}: ${width}x${height}`)\n }\n\n this.frameBuffer.resize(width, height)\n super.onResize(width, height)\n this.requestRender()\n }\n\n protected renderSelf(buffer: OptimizedBuffer): void {\n if (!this.visible || this.isDestroyed) return\n buffer.drawFrameBuffer(this.x, this.y, this.frameBuffer)\n }\n\n protected destroySelf(): void {\n // TODO: framebuffer collides with buffered Renderable, which holds a framebuffer\n // and destroys it if it exists already. Maybe instead of extending FrameBufferRenderable,\n // subclasses can use the buffered option on the base renderable instead,\n // then this would become something that takes in an external framebuffer to bring it into layout.\n this.frameBuffer?.destroy()\n super.destroySelf()\n }\n}\n",
9
9
  "import type { TextRenderable } from \".\"\nimport { BaseRenderable, type BaseRenderableOptions } from \"../Renderable\"\nimport { RGBA, parseColor } from \"../lib/RGBA\"\nimport { isStyledText, StyledText } from \"../lib/styled-text\"\nimport { type TextChunk } from \"../text-buffer\"\nimport type { RenderContext } from \"../types\"\n\nexport interface TextNodeOptions extends BaseRenderableOptions {\n fg?: string | RGBA\n bg?: string | RGBA\n attributes?: number\n}\n\nconst BrandedTextNodeRenderable: unique symbol = Symbol.for(\"@opentui/core/TextNodeRenderable\")\n\nexport function isTextNodeRenderable(obj: any): obj is TextNodeRenderable {\n return !!obj?.[BrandedTextNodeRenderable]\n}\n\nfunction styledTextToTextNodes(styledText: StyledText): TextNodeRenderable[] {\n return styledText.chunks.map((chunk) => {\n const node = new TextNodeRenderable({\n fg: chunk.fg,\n bg: chunk.bg,\n attributes: chunk.attributes,\n })\n node.add(chunk.text)\n return node\n })\n}\n\nexport class TextNodeRenderable extends BaseRenderable {\n [BrandedTextNodeRenderable] = true\n\n private _fg?: RGBA\n private _bg?: RGBA\n private _attributes: number\n private _children: (string | TextNodeRenderable)[] = []\n public parent: TextNodeRenderable | null = null\n\n constructor(options: TextNodeOptions) {\n super(options)\n\n this._fg = options.fg ? parseColor(options.fg) : undefined\n this._bg = options.bg ? parseColor(options.bg) : undefined\n this._attributes = options.attributes ?? 0\n }\n\n public get children(): (string | TextNodeRenderable)[] {\n return this._children\n }\n\n public set children(children: (string | TextNodeRenderable)[]) {\n this._children = children\n this.requestRender()\n }\n\n public requestRender(): void {\n this.markDirty()\n this.parent?.requestRender()\n }\n\n public add(obj: TextNodeRenderable | StyledText | string, index?: number): number {\n if (typeof obj === \"string\") {\n if (index !== undefined) {\n this._children.splice(index, 0, obj)\n this.requestRender()\n return index\n }\n\n const insertIndex = this._children.length\n this._children.push(obj)\n this.requestRender()\n return insertIndex\n }\n\n if (isTextNodeRenderable(obj)) {\n if (index !== undefined) {\n this._children.splice(index, 0, obj)\n obj.parent = this\n this.requestRender()\n return index\n }\n\n const insertIndex = this._children.length\n this._children.push(obj)\n obj.parent = this\n this.requestRender()\n return insertIndex\n }\n\n if (isStyledText(obj)) {\n const textNodes = styledTextToTextNodes(obj)\n if (index !== undefined) {\n this._children.splice(index, 0, ...textNodes)\n textNodes.forEach((node) => (node.parent = this))\n this.requestRender()\n return index\n }\n\n const insertIndex = this._children.length\n this._children.push(...textNodes)\n textNodes.forEach((node) => (node.parent = this))\n this.requestRender()\n return insertIndex\n }\n\n throw new Error(\"TextNodeRenderable only accepts strings, TextNodeRenderable instances, or StyledText instances\")\n }\n\n public replace(obj: TextNodeRenderable | string, index: number) {\n this._children[index] = obj\n if (typeof obj !== \"string\") {\n obj.parent = this\n }\n this.requestRender()\n }\n\n public insertBefore(\n child: string | TextNodeRenderable | StyledText,\n anchorNode: TextNodeRenderable | string | unknown,\n ): this {\n if (!anchorNode || !isTextNodeRenderable(anchorNode)) {\n throw new Error(\"Anchor must be a TextNodeRenderable\")\n }\n\n const anchorIndex = this._children.indexOf(anchorNode)\n if (anchorIndex === -1) {\n throw new Error(\"Anchor node not found in children\")\n }\n\n if (typeof child === \"string\") {\n this._children.splice(anchorIndex, 0, child)\n } else if (isTextNodeRenderable(child)) {\n this._children.splice(anchorIndex, 0, child)\n child.parent = this\n } else if (child instanceof StyledText) {\n const textNodes = styledTextToTextNodes(child)\n this._children.splice(anchorIndex, 0, ...textNodes)\n textNodes.forEach((node) => (node.parent = this))\n } else {\n throw new Error(\"Child must be a string, TextNodeRenderable, or StyledText instance\")\n }\n\n this.requestRender()\n return this\n }\n\n public remove(child: string | TextNodeRenderable): this {\n const childIndex = this._children.indexOf(child)\n if (childIndex === -1) {\n throw new Error(\"Child not found in children\")\n }\n\n this._children.splice(childIndex, 1)\n if (typeof child !== \"string\") {\n child.parent = null\n }\n this.requestRender()\n return this\n }\n\n public clear(): void {\n this._children = []\n this.requestRender()\n }\n\n public mergeStyles(parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number }): {\n fg?: RGBA\n bg?: RGBA\n attributes: number\n } {\n return {\n fg: this._fg ?? parentStyle.fg,\n bg: this._bg ?? parentStyle.bg,\n attributes: this._attributes | parentStyle.attributes,\n }\n }\n\n public gatherWithInheritedStyle(\n parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number } = { fg: undefined, bg: undefined, attributes: 0 },\n ): TextChunk[] {\n const currentStyle = this.mergeStyles(parentStyle)\n\n const chunks: TextChunk[] = []\n\n for (const child of this._children) {\n if (typeof child === \"string\") {\n chunks.push({\n __isChunk: true,\n text: child,\n fg: currentStyle.fg,\n bg: currentStyle.bg,\n attributes: currentStyle.attributes,\n })\n } else {\n const childChunks = child.gatherWithInheritedStyle(currentStyle)\n chunks.push(...childChunks)\n }\n }\n\n this.markClean()\n\n return chunks\n }\n\n public static fromString(text: string, options: Partial<TextNodeOptions> = {}): TextNodeRenderable {\n const node = new TextNodeRenderable(options)\n node.add(text)\n return node\n }\n\n public static fromNodes(nodes: TextNodeRenderable[], options: Partial<TextNodeOptions> = {}): TextNodeRenderable {\n const node = new TextNodeRenderable(options)\n for (const childNode of nodes) {\n node.add(childNode)\n }\n return node\n }\n\n public toChunks(\n parentStyle: { fg?: RGBA; bg?: RGBA; attributes: number } = { fg: undefined, bg: undefined, attributes: 0 },\n ): TextChunk[] {\n return this.gatherWithInheritedStyle(parentStyle)\n }\n\n public getChildren(): BaseRenderable[] {\n return this._children.filter((child): child is TextNodeRenderable => typeof child !== \"string\")\n }\n\n public getChildrenCount(): number {\n return this._children.length\n }\n\n public getRenderable(id: string): BaseRenderable | undefined {\n return this._children.find((child): child is TextNodeRenderable => typeof child !== \"string\" && child.id === id)\n }\n\n public get fg(): RGBA | undefined {\n return this._fg\n }\n\n public set fg(fg: RGBA | string | undefined) {\n if (!fg) {\n this._fg = undefined\n this.requestRender()\n return\n }\n this._fg = parseColor(fg)\n this.requestRender()\n }\n\n public set bg(bg: RGBA | string | undefined) {\n if (!bg) {\n this._bg = undefined\n this.requestRender()\n return\n }\n this._bg = parseColor(bg)\n this.requestRender()\n }\n\n public get bg(): RGBA | undefined {\n return this._bg\n }\n\n public set attributes(attributes: number) {\n this._attributes = attributes\n this.requestRender()\n }\n\n public get attributes(): number {\n return this._attributes\n }\n}\n\nexport class RootTextNodeRenderable extends TextNodeRenderable {\n textParent: TextRenderable\n\n constructor(\n private readonly ctx: RenderContext,\n options: TextNodeOptions,\n textParent: TextRenderable,\n ) {\n super(options)\n this.textParent = textParent\n }\n\n public requestRender(): void {\n this.markDirty()\n this.ctx.requestRender()\n }\n}\n",
10
- "import { BaseRenderable, Renderable, type RenderableOptions } from \"../Renderable\"\nimport { convertGlobalToLocalSelection, Selection, type LocalSelectionBounds } from \"../lib/selection\"\nimport { stringToStyledText, StyledText } from \"../lib/styled-text\"\nimport { TextBuffer, type TextChunk } from \"../text-buffer\"\nimport { RGBA, parseColor } from \"../lib/RGBA\"\nimport { type RenderContext } from \"../types\"\nimport type { OptimizedBuffer } from \"../buffer\"\nimport { Direction, MeasureMode } from \"yoga-layout\"\nimport { isTextNodeRenderable, RootTextNodeRenderable, TextNodeRenderable } from \"./TextNode\"\nimport type { LineInfo } from \"../zig\"\n\nexport interface TextOptions extends RenderableOptions<TextRenderable> {\n content?: StyledText | string\n fg?: string | RGBA\n bg?: string | RGBA\n selectionBg?: string | RGBA\n selectionFg?: string | RGBA\n selectable?: boolean\n attributes?: number\n wrap?: boolean\n wrapMode?: \"char\" | \"word\"\n}\n\nexport class TextRenderable extends Renderable {\n public selectable: boolean = true\n private _text: StyledText\n\n // TODO: The TextRenderable is currently juggling both a StyledText and a RootTextNodeRenderable.\n // We should refactor this to only use the RootTextNodeRenderable here and have a separate StyledTextRenderable with `content`.\n private _hasManualStyledText: boolean = false\n\n private _defaultFg: RGBA\n private _defaultBg: RGBA\n private _defaultAttributes: number\n private _selectionBg: RGBA | undefined\n private _selectionFg: RGBA | undefined\n private _wrap: boolean = false\n private _wrapMode: \"char\" | \"word\" = \"word\"\n private lastLocalSelection: LocalSelectionBounds | null = null\n\n private textBuffer: TextBuffer\n private _lineInfo: LineInfo = { lineStarts: [], lineWidths: [], maxLineWidth: 0 }\n\n protected rootTextNode: RootTextNodeRenderable\n\n protected _defaultOptions = {\n content: \"\",\n fg: RGBA.fromValues(1, 1, 1, 1),\n bg: RGBA.fromValues(0, 0, 0, 0),\n selectionBg: undefined,\n selectionFg: undefined,\n selectable: true,\n attributes: 0,\n wrap: true,\n wrapMode: \"word\" as \"char\" | \"word\",\n } satisfies Partial<TextOptions>\n\n constructor(ctx: RenderContext, options: TextOptions) {\n super(ctx, options)\n\n const content = options.content ?? this._defaultOptions.content\n const styledText = typeof content === \"string\" ? stringToStyledText(content) : content\n this._text = styledText\n this._hasManualStyledText = !!options.content\n this._defaultFg = parseColor(options.fg ?? this._defaultOptions.fg)\n this._defaultBg = parseColor(options.bg ?? this._defaultOptions.bg)\n this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes\n this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : this._defaultOptions.selectionBg\n this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : this._defaultOptions.selectionFg\n this.selectable = options.selectable ?? this._defaultOptions.selectable\n this._wrap = options.wrap ?? this._defaultOptions.wrap\n this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode\n\n this.textBuffer = TextBuffer.create(this._ctx.widthMethod)\n\n this.textBuffer.setWrapMode(this._wrapMode)\n this.setupMeasureFunc()\n\n this.textBuffer.setDefaultFg(this._defaultFg)\n this.textBuffer.setDefaultBg(this._defaultBg)\n this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n\n this.rootTextNode = new RootTextNodeRenderable(\n ctx,\n {\n id: `${this.id}-root`,\n fg: this._defaultFg,\n bg: this._defaultBg,\n attributes: this._defaultAttributes,\n },\n this,\n )\n\n this.updateTextBuffer(styledText)\n this._text.mount(this)\n\n if (this._wrap && this.width > 0) {\n this.updateWrapWidth(this.width)\n }\n\n this.updateTextInfo()\n }\n\n private updateTextBuffer(styledText: StyledText): void {\n this.textBuffer.setStyledText(styledText)\n this.clearChunks(styledText)\n }\n\n private clearChunks(styledText: StyledText): void {\n // Clearing chunks that were already writtend to the text buffer,\n // to not retain references to the text data in js\n // TODO: This is causing issues in the solid renderer\n // styledText.chunks.forEach((chunk) => {\n // // @ts-ignore\n // chunk.text = undefined\n // })\n }\n\n get content(): StyledText {\n return this._text\n }\n\n get plainText(): string {\n return this.textBuffer.getPlainText()\n }\n\n get textLength(): number {\n return this.textBuffer.length\n }\n\n get chunks(): TextChunk[] {\n return this._text.chunks\n }\n\n get textNode(): RootTextNodeRenderable {\n return this.rootTextNode\n }\n\n set content(value: StyledText | string) {\n this._hasManualStyledText = true\n const styledText = typeof value === \"string\" ? stringToStyledText(value) : value\n if (this._text !== styledText) {\n this._text = styledText\n styledText.mount(this)\n this.updateTextBuffer(styledText)\n this.updateTextInfo()\n }\n }\n\n get fg(): RGBA {\n return this._defaultFg\n }\n\n set fg(value: RGBA | string | undefined) {\n const newColor = parseColor(value ?? this._defaultOptions.fg)\n this.rootTextNode.fg = newColor\n if (this._defaultFg !== newColor) {\n this._defaultFg = newColor\n this.textBuffer.setDefaultFg(this._defaultFg)\n this.rootTextNode.fg = newColor\n this.requestRender()\n }\n }\n\n get selectionBg(): RGBA | undefined {\n return this._selectionBg\n }\n\n set selectionBg(value: RGBA | string | undefined) {\n const newColor = value ? parseColor(value) : this._defaultOptions.selectionBg\n if (this._selectionBg !== newColor) {\n this._selectionBg = newColor\n if (this.lastLocalSelection) {\n this.updateLocalSelection(this.lastLocalSelection)\n }\n this.requestRender()\n }\n }\n\n get selectionFg(): RGBA | undefined {\n return this._selectionFg\n }\n\n set selectionFg(value: RGBA | string | undefined) {\n const newColor = value ? parseColor(value) : this._defaultOptions.selectionFg\n if (this._selectionFg !== newColor) {\n this._selectionFg = newColor\n if (this.lastLocalSelection) {\n this.updateLocalSelection(this.lastLocalSelection)\n }\n this.requestRender()\n }\n }\n\n get bg(): RGBA {\n return this._defaultBg\n }\n\n set bg(value: RGBA | string | undefined) {\n const newColor = parseColor(value ?? this._defaultOptions.bg)\n this.rootTextNode.bg = newColor\n if (this._defaultBg !== newColor) {\n this._defaultBg = newColor\n this.textBuffer.setDefaultBg(this._defaultBg)\n this.rootTextNode.bg = newColor\n this.requestRender()\n }\n }\n\n get attributes(): number {\n return this._defaultAttributes\n }\n\n set attributes(value: number) {\n if (this._defaultAttributes !== value) {\n this._defaultAttributes = value\n this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n this.rootTextNode.attributes = value\n this.requestRender()\n }\n }\n\n get wrap(): boolean {\n return this._wrap\n }\n\n set wrap(value: boolean) {\n if (this._wrap !== value) {\n this._wrap = value\n // Set or clear wrap width based on current setting\n this.textBuffer.setWrapWidth(this._wrap ? this.width : null)\n this.requestRender()\n }\n }\n\n get wrapMode(): \"char\" | \"word\" {\n return this._wrapMode\n }\n\n set wrapMode(value: \"char\" | \"word\") {\n if (this._wrapMode !== value) {\n this._wrapMode = value\n this.textBuffer.setWrapMode(this._wrapMode)\n this.requestRender()\n }\n }\n\n protected onResize(width: number, height: number): void {\n if (this.lastLocalSelection) {\n const changed = this.updateLocalSelection(this.lastLocalSelection)\n if (changed) {\n this.requestRender()\n }\n }\n }\n\n private updateLocalSelection(localSelection: LocalSelectionBounds | null): boolean {\n if (!localSelection?.isActive) {\n this.textBuffer.resetLocalSelection()\n return true\n }\n\n return this.textBuffer.setLocalSelection(\n localSelection.anchorX,\n localSelection.anchorY,\n localSelection.focusX,\n localSelection.focusY,\n this._selectionBg,\n this._selectionFg,\n )\n }\n\n private updateTextInfo(): void {\n if (this.lastLocalSelection) {\n const changed = this.updateLocalSelection(this.lastLocalSelection)\n if (changed) {\n this.requestRender()\n }\n }\n\n this.yogaNode.markDirty()\n this.requestRender()\n }\n\n private updateLineInfo(): void {\n const lineInfo = this.textBuffer.lineInfo\n this._lineInfo.lineStarts = lineInfo.lineStarts\n this._lineInfo.lineWidths = lineInfo.lineWidths\n this._lineInfo.maxLineWidth = lineInfo.maxLineWidth\n }\n\n private updateWrapWidth(width: number): void {\n this.textBuffer.setWrapWidth(width)\n this.updateLineInfo()\n }\n\n private setupMeasureFunc(): void {\n const measureFunc = (\n width: number,\n widthMode: MeasureMode,\n height: number,\n heightMode: MeasureMode,\n ): { width: number; height: number } => {\n if (this._wrap) {\n if (this.width !== width) {\n this.updateWrapWidth(width)\n }\n } else {\n this.updateLineInfo()\n }\n\n const measuredWidth = this._lineInfo.maxLineWidth\n const measuredHeight = this._lineInfo.lineStarts.length\n\n // NOTE: Yoga may use these measurements or not.\n // If the yoga node settings and the parent allow this node to grow, it will.\n return {\n width: Math.max(1, measuredWidth),\n height: Math.max(1, measuredHeight),\n }\n }\n\n this.yogaNode.setMeasureFunc(measureFunc)\n }\n\n insertChunk(chunk: TextChunk, index?: number): void {\n this.textBuffer.insertChunkGroup(\n index ?? this.textBuffer.chunkGroupCount,\n chunk.text,\n chunk.fg,\n chunk.bg,\n chunk.attributes,\n )\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n removeChunk(chunk: TextChunk): void {\n const index = this._text.chunks.indexOf(chunk)\n if (index === -1) return\n this.textBuffer.removeChunkGroup(index)\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n replaceChunk(chunk: TextChunk, oldChunk: TextChunk): void {\n const index = this._text.chunks.indexOf(oldChunk)\n\n if (index === -1) return\n this.textBuffer.replaceChunkGroup(index, chunk.text, chunk.fg, chunk.bg, chunk.attributes)\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n private updateTextFromNodes(): void {\n if (this.rootTextNode.isDirty && !this._hasManualStyledText) {\n const chunks = this.rootTextNode.gatherWithInheritedStyle({\n fg: this._defaultFg,\n bg: this._defaultBg,\n attributes: this._defaultAttributes,\n })\n this.textBuffer.setStyledText(new StyledText(chunks))\n this.updateTextInfo()\n }\n }\n\n public add(obj: TextNodeRenderable | StyledText | string, index?: number): number {\n return this.rootTextNode.add(obj, index)\n }\n\n public remove(id: string): void {\n const child = this.rootTextNode.getRenderable(id)\n if (child && isTextNodeRenderable(child)) {\n this.rootTextNode.remove(child)\n }\n }\n\n public insertBefore(obj: BaseRenderable | any, anchor?: TextNodeRenderable): number {\n this.rootTextNode.insertBefore(obj, anchor)\n return this.rootTextNode.children.indexOf(obj)\n }\n\n public getTextChildren(): BaseRenderable[] {\n return this.rootTextNode.getChildren()\n }\n\n public clear(): void {\n this.rootTextNode.clear()\n\n const emptyStyledText = stringToStyledText(\"\")\n this._text = emptyStyledText\n emptyStyledText.mount(this)\n this.updateTextBuffer(emptyStyledText)\n this.updateTextInfo()\n\n this.requestRender()\n }\n\n shouldStartSelection(x: number, y: number): boolean {\n if (!this.selectable) return false\n\n const localX = x - this.x\n const localY = y - this.y\n\n return localX >= 0 && localX < this.width && localY >= 0 && localY < this.height\n }\n\n onSelectionChanged(selection: Selection | null): boolean {\n const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n this.lastLocalSelection = localSelection\n\n const changed = this.updateLocalSelection(localSelection)\n\n if (changed) {\n this.requestRender()\n }\n\n return this.hasSelection()\n }\n\n getSelectedText(): string {\n return this.textBuffer.getSelectedText()\n }\n\n hasSelection(): boolean {\n return this.textBuffer.hasSelection()\n }\n\n getSelection(): { start: number; end: number } | null {\n return this.textBuffer.getSelection()\n }\n\n public onLifecyclePass = () => {\n this.updateTextFromNodes()\n }\n\n render(buffer: OptimizedBuffer, deltaTime: number): void {\n if (!this.visible) return\n\n this.markClean()\n this._ctx.addToHitGrid(this.x, this.y, this.width, this.height, this.num)\n\n this.renderSelf(buffer)\n }\n\n protected renderSelf(buffer: OptimizedBuffer): void {\n if (this.textBuffer.ptr) {\n const clipRect = {\n x: this.x,\n y: this.y,\n width: this.width,\n height: this.height,\n }\n\n buffer.drawTextBuffer(this.textBuffer, this.x, this.y, clipRect)\n }\n }\n\n destroy(): void {\n this.textBuffer.destroy()\n this.rootTextNode.children.length = 0\n super.destroy()\n }\n}\n",
10
+ "import { BaseRenderable, Renderable, type RenderableOptions } from \"../Renderable\"\nimport { convertGlobalToLocalSelection, Selection, type LocalSelectionBounds } from \"../lib/selection\"\nimport { stringToStyledText, StyledText } from \"../lib/styled-text\"\nimport { TextBuffer, type TextChunk } from \"../text-buffer\"\nimport { RGBA, parseColor } from \"../lib/RGBA\"\nimport { type RenderContext } from \"../types\"\nimport type { OptimizedBuffer } from \"../buffer\"\nimport { MeasureMode } from \"yoga-layout\"\nimport { isTextNodeRenderable, RootTextNodeRenderable, TextNodeRenderable } from \"./TextNode\"\nimport type { LineInfo } from \"../zig\"\n\nexport interface TextOptions extends RenderableOptions<TextRenderable> {\n content?: StyledText | string\n fg?: string | RGBA\n bg?: string | RGBA\n selectionBg?: string | RGBA\n selectionFg?: string | RGBA\n selectable?: boolean\n attributes?: number\n wrap?: boolean\n wrapMode?: \"char\" | \"word\"\n}\n\nexport class TextRenderable extends Renderable {\n public selectable: boolean = true\n private _text: StyledText\n\n // TODO: The TextRenderable is currently juggling both a StyledText and a RootTextNodeRenderable.\n // We should refactor this to only use the RootTextNodeRenderable here and have a separate StyledTextRenderable with `content`.\n private _hasManualStyledText: boolean = false\n\n private _defaultFg: RGBA\n private _defaultBg: RGBA\n private _defaultAttributes: number\n private _selectionBg: RGBA | undefined\n private _selectionFg: RGBA | undefined\n private _wrap: boolean = false\n private _wrapMode: \"char\" | \"word\" = \"word\"\n private lastLocalSelection: LocalSelectionBounds | null = null\n\n private textBuffer: TextBuffer\n private _lineInfo: LineInfo = { lineStarts: [], lineWidths: [], maxLineWidth: 0 }\n\n protected rootTextNode: RootTextNodeRenderable\n\n protected _defaultOptions = {\n content: \"\",\n fg: RGBA.fromValues(1, 1, 1, 1),\n bg: RGBA.fromValues(0, 0, 0, 0),\n selectionBg: undefined,\n selectionFg: undefined,\n selectable: true,\n attributes: 0,\n wrap: true,\n wrapMode: \"word\" as \"char\" | \"word\",\n } satisfies Partial<TextOptions>\n\n constructor(ctx: RenderContext, options: TextOptions) {\n super(ctx, options)\n\n const content = options.content ?? this._defaultOptions.content\n const styledText = typeof content === \"string\" ? stringToStyledText(content) : content\n this._text = styledText\n this._hasManualStyledText = !!options.content\n this._defaultFg = parseColor(options.fg ?? this._defaultOptions.fg)\n this._defaultBg = parseColor(options.bg ?? this._defaultOptions.bg)\n this._defaultAttributes = options.attributes ?? this._defaultOptions.attributes\n this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : this._defaultOptions.selectionBg\n this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : this._defaultOptions.selectionFg\n this.selectable = options.selectable ?? this._defaultOptions.selectable\n this._wrap = options.wrap ?? this._defaultOptions.wrap\n this._wrapMode = options.wrapMode ?? this._defaultOptions.wrapMode\n\n this.textBuffer = TextBuffer.create(this._ctx.widthMethod)\n\n this.textBuffer.setWrapMode(this._wrapMode)\n this.setupMeasureFunc()\n\n this.textBuffer.setDefaultFg(this._defaultFg)\n this.textBuffer.setDefaultBg(this._defaultBg)\n this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n\n this.rootTextNode = new RootTextNodeRenderable(\n ctx,\n {\n id: `${this.id}-root`,\n fg: this._defaultFg,\n bg: this._defaultBg,\n attributes: this._defaultAttributes,\n },\n this,\n )\n\n this.updateTextBuffer(styledText)\n this._text.mount(this)\n\n if (this._wrap && this.width > 0) {\n this.updateWrapWidth(this.width)\n }\n\n this.updateTextInfo()\n }\n\n private updateTextBuffer(styledText: StyledText): void {\n this.textBuffer.setStyledText(styledText)\n this.clearChunks(styledText)\n }\n\n private clearChunks(styledText: StyledText): void {\n // Clearing chunks that were already writtend to the text buffer,\n // to not retain references to the text data in js\n // TODO: This is causing issues in the solid renderer\n // styledText.chunks.forEach((chunk) => {\n // // @ts-ignore\n // chunk.text = undefined\n // })\n }\n\n get content(): StyledText {\n return this._text\n }\n\n get plainText(): string {\n return this.textBuffer.getPlainText()\n }\n\n get textLength(): number {\n return this.textBuffer.length\n }\n\n get chunks(): TextChunk[] {\n return this._text.chunks\n }\n\n get textNode(): RootTextNodeRenderable {\n return this.rootTextNode\n }\n\n set content(value: StyledText | string) {\n this._hasManualStyledText = true\n const styledText = typeof value === \"string\" ? stringToStyledText(value) : value\n if (this._text !== styledText) {\n this._text = styledText\n styledText.mount(this)\n this.updateTextBuffer(styledText)\n this.updateTextInfo()\n }\n }\n\n get fg(): RGBA {\n return this._defaultFg\n }\n\n set fg(value: RGBA | string | undefined) {\n const newColor = parseColor(value ?? this._defaultOptions.fg)\n this.rootTextNode.fg = newColor\n if (this._defaultFg !== newColor) {\n this._defaultFg = newColor\n this.textBuffer.setDefaultFg(this._defaultFg)\n this.rootTextNode.fg = newColor\n this.requestRender()\n }\n }\n\n get selectionBg(): RGBA | undefined {\n return this._selectionBg\n }\n\n set selectionBg(value: RGBA | string | undefined) {\n const newColor = value ? parseColor(value) : this._defaultOptions.selectionBg\n if (this._selectionBg !== newColor) {\n this._selectionBg = newColor\n if (this.lastLocalSelection) {\n this.updateLocalSelection(this.lastLocalSelection)\n }\n this.requestRender()\n }\n }\n\n get selectionFg(): RGBA | undefined {\n return this._selectionFg\n }\n\n set selectionFg(value: RGBA | string | undefined) {\n const newColor = value ? parseColor(value) : this._defaultOptions.selectionFg\n if (this._selectionFg !== newColor) {\n this._selectionFg = newColor\n if (this.lastLocalSelection) {\n this.updateLocalSelection(this.lastLocalSelection)\n }\n this.requestRender()\n }\n }\n\n get bg(): RGBA {\n return this._defaultBg\n }\n\n set bg(value: RGBA | string | undefined) {\n const newColor = parseColor(value ?? this._defaultOptions.bg)\n this.rootTextNode.bg = newColor\n if (this._defaultBg !== newColor) {\n this._defaultBg = newColor\n this.textBuffer.setDefaultBg(this._defaultBg)\n this.rootTextNode.bg = newColor\n this.requestRender()\n }\n }\n\n get attributes(): number {\n return this._defaultAttributes\n }\n\n set attributes(value: number) {\n if (this._defaultAttributes !== value) {\n this._defaultAttributes = value\n this.textBuffer.setDefaultAttributes(this._defaultAttributes)\n this.rootTextNode.attributes = value\n this.requestRender()\n }\n }\n\n get wrap(): boolean {\n return this._wrap\n }\n\n set wrap(value: boolean) {\n if (this._wrap !== value) {\n this._wrap = value\n // Set or clear wrap width based on current setting\n this.textBuffer.setWrapWidth(this._wrap ? this.width : null)\n this.requestRender()\n }\n }\n\n get wrapMode(): \"char\" | \"word\" {\n return this._wrapMode\n }\n\n set wrapMode(value: \"char\" | \"word\") {\n if (this._wrapMode !== value) {\n this._wrapMode = value\n this.textBuffer.setWrapMode(this._wrapMode)\n this.requestRender()\n }\n }\n\n protected onResize(width: number, height: number): void {\n if (this.lastLocalSelection) {\n const changed = this.updateLocalSelection(this.lastLocalSelection)\n if (changed) {\n this.requestRender()\n }\n }\n }\n\n private updateLocalSelection(localSelection: LocalSelectionBounds | null): boolean {\n if (!localSelection?.isActive) {\n this.textBuffer.resetLocalSelection()\n return true\n }\n\n return this.textBuffer.setLocalSelection(\n localSelection.anchorX,\n localSelection.anchorY,\n localSelection.focusX,\n localSelection.focusY,\n this._selectionBg,\n this._selectionFg,\n )\n }\n\n private updateTextInfo(): void {\n if (this.lastLocalSelection) {\n const changed = this.updateLocalSelection(this.lastLocalSelection)\n if (changed) {\n this.requestRender()\n }\n }\n\n this.yogaNode.markDirty()\n this.requestRender()\n }\n\n private updateLineInfo(): void {\n const lineInfo = this.textBuffer.lineInfo\n this._lineInfo.lineStarts = lineInfo.lineStarts\n this._lineInfo.lineWidths = lineInfo.lineWidths\n this._lineInfo.maxLineWidth = lineInfo.maxLineWidth\n }\n\n private updateWrapWidth(width: number): void {\n this.textBuffer.setWrapWidth(width)\n this.updateLineInfo()\n }\n\n private setupMeasureFunc(): void {\n const measureFunc = (\n width: number,\n widthMode: MeasureMode,\n height: number,\n heightMode: MeasureMode,\n ): { width: number; height: number } => {\n if (this._wrap) {\n if (this.width !== width) {\n this.updateWrapWidth(width)\n }\n } else {\n this.updateLineInfo()\n }\n\n const measuredWidth = this._lineInfo.maxLineWidth\n const measuredHeight = this._lineInfo.lineStarts.length\n\n // NOTE: Yoga may use these measurements or not.\n // If the yoga node settings and the parent allow this node to grow, it will.\n return {\n width: Math.max(1, measuredWidth),\n height: Math.max(1, measuredHeight),\n }\n }\n\n this.yogaNode.setMeasureFunc(measureFunc)\n }\n\n insertChunk(chunk: TextChunk, index?: number): void {\n this.textBuffer.insertChunkGroup(\n index ?? this.textBuffer.chunkGroupCount,\n chunk.text,\n chunk.fg,\n chunk.bg,\n chunk.attributes,\n )\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n removeChunk(chunk: TextChunk): void {\n const index = this._text.chunks.indexOf(chunk)\n if (index === -1) return\n this.textBuffer.removeChunkGroup(index)\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n replaceChunk(chunk: TextChunk, oldChunk: TextChunk): void {\n const index = this._text.chunks.indexOf(oldChunk)\n\n if (index === -1) return\n this.textBuffer.replaceChunkGroup(index, chunk.text, chunk.fg, chunk.bg, chunk.attributes)\n this.updateTextInfo()\n this.clearChunks(this._text)\n }\n\n private updateTextFromNodes(): void {\n if (this.rootTextNode.isDirty && !this._hasManualStyledText) {\n const chunks = this.rootTextNode.gatherWithInheritedStyle({\n fg: this._defaultFg,\n bg: this._defaultBg,\n attributes: this._defaultAttributes,\n })\n this.textBuffer.setStyledText(new StyledText(chunks))\n this.updateTextInfo()\n }\n }\n\n public add(obj: TextNodeRenderable | StyledText | string, index?: number): number {\n return this.rootTextNode.add(obj, index)\n }\n\n public remove(id: string): void {\n const child = this.rootTextNode.getRenderable(id)\n if (child && isTextNodeRenderable(child)) {\n this.rootTextNode.remove(child)\n }\n }\n\n public insertBefore(obj: BaseRenderable | any, anchor?: TextNodeRenderable): number {\n this.rootTextNode.insertBefore(obj, anchor)\n return this.rootTextNode.children.indexOf(obj)\n }\n\n public getTextChildren(): BaseRenderable[] {\n return this.rootTextNode.getChildren()\n }\n\n public clear(): void {\n this.rootTextNode.clear()\n\n const emptyStyledText = stringToStyledText(\"\")\n this._text = emptyStyledText\n emptyStyledText.mount(this)\n this.updateTextBuffer(emptyStyledText)\n this.updateTextInfo()\n\n this.requestRender()\n }\n\n shouldStartSelection(x: number, y: number): boolean {\n if (!this.selectable) return false\n\n const localX = x - this.x\n const localY = y - this.y\n\n return localX >= 0 && localX < this.width && localY >= 0 && localY < this.height\n }\n\n onSelectionChanged(selection: Selection | null): boolean {\n const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n this.lastLocalSelection = localSelection\n\n const changed = this.updateLocalSelection(localSelection)\n\n if (changed) {\n this.requestRender()\n }\n\n return this.hasSelection()\n }\n\n getSelectedText(): string {\n return this.textBuffer.getSelectedText()\n }\n\n hasSelection(): boolean {\n return this.textBuffer.hasSelection()\n }\n\n getSelection(): { start: number; end: number } | null {\n return this.textBuffer.getSelection()\n }\n\n public onLifecyclePass = () => {\n this.updateTextFromNodes()\n }\n\n render(buffer: OptimizedBuffer, deltaTime: number): void {\n if (!this.visible) return\n\n this.markClean()\n this._ctx.addToHitGrid(this.x, this.y, this.width, this.height, this.num)\n\n this.renderSelf(buffer)\n }\n\n protected renderSelf(buffer: OptimizedBuffer): void {\n if (this.textBuffer.ptr) {\n const clipRect = {\n x: this.x,\n y: this.y,\n width: this.width,\n height: this.height,\n }\n\n buffer.drawTextBuffer(this.textBuffer, this.x, this.y, clipRect)\n }\n }\n\n destroy(): void {\n this.textBuffer.destroy()\n this.rootTextNode.children.length = 0\n super.destroy()\n }\n}\n",
11
11
  "import type { RenderableOptions } from \"../Renderable\"\nimport {\n ASCIIFontSelectionHelper,\n convertGlobalToLocalSelection,\n Selection,\n type LocalSelectionBounds,\n} from \"../lib/selection\"\nimport {\n type fonts,\n measureText,\n renderFontToFrameBuffer,\n getCharacterPositions,\n type ASCIIFontName,\n} from \"../lib/ascii.font\"\nimport { RGBA, parseColor } from \"../lib/RGBA\"\nimport { FrameBufferRenderable, type FrameBufferOptions } from \"./FrameBuffer\"\nimport type { RenderContext } from \"../types\"\n\nexport interface ASCIIFontOptions extends Omit<RenderableOptions<ASCIIFontRenderable>, \"width\" | \"height\"> {\n text?: string\n font?: ASCIIFontName\n fg?: RGBA | RGBA[]\n bg?: RGBA\n selectionBg?: string | RGBA\n selectionFg?: string | RGBA\n selectable?: boolean\n}\n\nexport class ASCIIFontRenderable extends FrameBufferRenderable {\n public selectable: boolean = true\n private _text: string\n private _font: keyof typeof fonts\n private _fg: RGBA[]\n private _bg: RGBA\n private _selectionBg: RGBA | undefined\n private _selectionFg: RGBA | undefined\n private lastLocalSelection: LocalSelectionBounds | null = null\n\n private selectionHelper: ASCIIFontSelectionHelper\n\n constructor(ctx: RenderContext, options: ASCIIFontOptions) {\n const font = options.font || \"tiny\"\n const text = options.text || \"\"\n const measurements = measureText({ text: text, font })\n\n super(ctx, {\n flexShrink: 0,\n ...options,\n width: measurements.width || 1,\n height: measurements.height || 1,\n respectAlpha: true,\n } as FrameBufferOptions)\n\n this._text = text\n this._font = font\n this._fg = Array.isArray(options.fg) ? options.fg : [options.fg || RGBA.fromInts(255, 255, 255, 255)]\n this._bg = options.bg || RGBA.fromValues(0, 0, 0, 0)\n this._selectionBg = options.selectionBg ? parseColor(options.selectionBg) : undefined\n this._selectionFg = options.selectionFg ? parseColor(options.selectionFg) : undefined\n this.selectable = options.selectable ?? true\n\n this.selectionHelper = new ASCIIFontSelectionHelper(\n () => this._text,\n () => this._font,\n )\n\n this.renderFontToBuffer()\n }\n\n get text(): string {\n return this._text\n }\n\n set text(value: string) {\n this._text = value\n this.updateDimensions()\n\n if (this.lastLocalSelection) {\n this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height)\n }\n\n this.renderFontToBuffer()\n this.requestRender()\n }\n\n get font(): keyof typeof fonts {\n return this._font\n }\n\n set font(value: keyof typeof fonts) {\n this._font = value\n this.updateDimensions()\n\n if (this.lastLocalSelection) {\n this.selectionHelper.onLocalSelectionChanged(this.lastLocalSelection, this.width, this.height)\n }\n\n this.renderFontToBuffer()\n this.requestRender()\n }\n\n get fg(): RGBA[] {\n return this._fg\n }\n\n set fg(value: RGBA | RGBA[] | string | string[]) {\n if (Array.isArray(value)) {\n this._fg = value.map((color) => (typeof color === \"string\" ? parseColor(color) : color))\n } else {\n this._fg = [typeof value === \"string\" ? parseColor(value) : value]\n }\n\n this.renderFontToBuffer()\n this.requestRender()\n }\n\n get bg(): RGBA {\n return this._bg\n }\n\n set bg(value: RGBA | string) {\n this._bg = typeof value === \"string\" ? parseColor(value) : value\n this.renderFontToBuffer()\n this.requestRender()\n }\n\n private updateDimensions(): void {\n const measurements = measureText({ text: this._text, font: this._font })\n this.width = measurements.width\n this.height = measurements.height\n }\n\n shouldStartSelection(x: number, y: number): boolean {\n const localX = x - this.x\n const localY = y - this.y\n return this.selectionHelper.shouldStartSelection(localX, localY, this.width, this.height)\n }\n\n onSelectionChanged(selection: Selection | null): boolean {\n const localSelection = convertGlobalToLocalSelection(selection, this.x, this.y)\n this.lastLocalSelection = localSelection\n const changed = this.selectionHelper.onLocalSelectionChanged(localSelection, this.width, this.height)\n if (changed) {\n this.renderFontToBuffer()\n this.requestRender()\n }\n return changed\n }\n\n getSelectedText(): string {\n const selection = this.selectionHelper.getSelection()\n if (!selection) return \"\"\n return this._text.slice(selection.start, selection.end)\n }\n\n hasSelection(): boolean {\n return this.selectionHelper.hasSelection()\n }\n\n protected onResize(width: number, height: number): void {\n super.onResize(width, height)\n this.renderFontToBuffer()\n }\n\n private renderFontToBuffer(): void {\n if (this.isDestroyed) return\n this.frameBuffer.clear(this._bg)\n\n renderFontToFrameBuffer(this.frameBuffer, {\n text: this._text,\n x: 0,\n y: 0,\n fg: this._fg,\n bg: this._bg,\n font: this._font,\n })\n\n const selection = this.selectionHelper.getSelection()\n if (selection && (this._selectionBg || this._selectionFg)) {\n this.renderSelectionHighlight(selection)\n }\n }\n\n private renderSelectionHighlight(selection: { start: number; end: number }): void {\n if (!this._selectionBg && !this._selectionFg) return\n\n const selectedText = this._text.slice(selection.start, selection.end)\n if (!selectedText) return\n\n const positions = getCharacterPositions(this._text, this._font)\n const startX = positions[selection.start] || 0\n const endX =\n selection.end < positions.length\n ? positions[selection.end]\n : measureText({ text: this._text, font: this._font }).width\n\n if (this._selectionBg) {\n this.frameBuffer.fillRect(startX, 0, endX - startX, this.height, this._selectionBg)\n }\n\n if (this._selectionFg || this._selectionBg) {\n renderFontToFrameBuffer(this.frameBuffer, {\n text: selectedText,\n x: startX,\n y: 0,\n fg: this._selectionFg ? [this._selectionFg] : this._fg,\n bg: this._selectionBg || this._bg,\n font: this._font,\n })\n }\n }\n}\n",
12
12
  "import { OptimizedBuffer } from \"../buffer\"\nimport type { ParsedKey } from \"../lib/parse.keypress\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA\"\nimport { Renderable, type RenderableOptions } from \"../Renderable\"\nimport type { RenderContext } from \"../types\"\n\nexport interface InputRenderableOptions extends RenderableOptions<InputRenderable> {\n backgroundColor?: ColorInput\n textColor?: ColorInput\n focusedBackgroundColor?: ColorInput\n focusedTextColor?: ColorInput\n placeholder?: string\n placeholderColor?: ColorInput\n cursorColor?: ColorInput\n maxLength?: number\n value?: string\n}\n\n// TODO: make this just plain strings instead of an enum (same for other events)\nexport enum InputRenderableEvents {\n INPUT = \"input\",\n CHANGE = \"change\",\n ENTER = \"enter\",\n}\n\nexport class InputRenderable extends Renderable {\n protected _focusable: boolean = true\n\n private _value: string = \"\"\n private _cursorPosition: number = 0\n private _placeholder: string\n private _backgroundColor: RGBA\n private _textColor: RGBA\n private _focusedBackgroundColor: RGBA\n private _focusedTextColor: RGBA\n private _placeholderColor: RGBA\n private _cursorColor: RGBA\n private _maxLength: number\n private _lastCommittedValue: string = \"\"\n\n protected _defaultOptions = {\n backgroundColor: \"transparent\",\n textColor: \"#FFFFFF\",\n focusedBackgroundColor: \"#1a1a1a\",\n focusedTextColor: \"#FFFFFF\",\n placeholder: \"\",\n placeholderColor: \"#666666\",\n cursorColor: \"#FFFFFF\",\n maxLength: 1000,\n value: \"\",\n } satisfies Partial<InputRenderableOptions>\n\n constructor(ctx: RenderContext, options: InputRenderableOptions) {\n super(ctx, { ...options, buffered: true })\n\n this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor)\n this._textColor = parseColor(options.textColor || this._defaultOptions.textColor)\n this._focusedBackgroundColor = parseColor(\n options.focusedBackgroundColor || options.backgroundColor || this._defaultOptions.focusedBackgroundColor,\n )\n this._focusedTextColor = parseColor(\n options.focusedTextColor || options.textColor || this._defaultOptions.focusedTextColor,\n )\n this._placeholder = options.placeholder || this._defaultOptions.placeholder\n this._value = options.value || this._defaultOptions.value\n this._lastCommittedValue = this._value\n this._cursorPosition = this._value.length\n this._maxLength = options.maxLength || this._defaultOptions.maxLength\n\n this._placeholderColor = parseColor(options.placeholderColor || this._defaultOptions.placeholderColor)\n this._cursorColor = parseColor(options.cursorColor || this._defaultOptions.cursorColor)\n }\n\n private updateCursorPosition(): void {\n if (!this._focused) return\n\n const contentX = 0\n const contentY = 0\n const contentWidth = this.width\n\n const maxVisibleChars = contentWidth - 1\n let displayStartIndex = 0\n\n if (this._cursorPosition >= maxVisibleChars) {\n displayStartIndex = this._cursorPosition - maxVisibleChars + 1\n }\n\n const cursorDisplayX = this._cursorPosition - displayStartIndex\n\n if (cursorDisplayX >= 0 && cursorDisplayX < contentWidth) {\n const absoluteCursorX = this.x + contentX + cursorDisplayX + 1\n const absoluteCursorY = this.y + contentY + 1\n\n this._ctx.setCursorPosition(absoluteCursorX, absoluteCursorY, true)\n this._ctx.setCursorColor(this._cursorColor)\n }\n }\n\n public focus(): void {\n super.focus()\n this._ctx.setCursorStyle(\"block\", true)\n this._ctx.setCursorColor(this._cursorColor)\n this.updateCursorPosition()\n }\n\n public blur(): void {\n super.blur()\n this._ctx.setCursorPosition(0, 0, false)\n\n if (this._value !== this._lastCommittedValue) {\n this._lastCommittedValue = this._value\n this.emit(InputRenderableEvents.CHANGE, this._value)\n }\n }\n\n protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n if (!this.visible || !this.frameBuffer) return\n\n if (this.isDirty) {\n this.refreshFrameBuffer()\n }\n }\n\n private refreshFrameBuffer(): void {\n if (!this.frameBuffer) return\n\n const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor\n this.frameBuffer.clear(bgColor)\n\n const contentX = 0\n const contentY = 0\n const contentWidth = this.width\n const contentHeight = this.height\n\n const displayText = this._value || this._placeholder\n const isPlaceholder = !this._value && this._placeholder\n const baseTextColor = this._focused ? this._focusedTextColor : this._textColor\n const textColor = isPlaceholder ? this._placeholderColor : baseTextColor\n\n const maxVisibleChars = contentWidth - 1\n let displayStartIndex = 0\n\n if (this._cursorPosition >= maxVisibleChars) {\n displayStartIndex = this._cursorPosition - maxVisibleChars + 1\n }\n\n const visibleText = displayText.substring(displayStartIndex, displayStartIndex + maxVisibleChars)\n\n if (visibleText) {\n this.frameBuffer.drawText(visibleText, contentX, contentY, textColor)\n }\n\n if (this._focused) {\n this.updateCursorPosition()\n }\n }\n\n public get value(): string {\n return this._value\n }\n\n public set value(value: string) {\n const newValue = value.substring(0, this._maxLength)\n if (this._value !== newValue) {\n this._value = newValue\n this._cursorPosition = Math.min(this._cursorPosition, this._value.length)\n this.requestRender()\n this.updateCursorPosition()\n this.emit(InputRenderableEvents.INPUT, this._value)\n }\n }\n\n public set placeholder(placeholder: string) {\n if (this._placeholder !== placeholder) {\n this._placeholder = placeholder\n this.requestRender()\n }\n }\n\n public get cursorPosition(): number {\n return this._cursorPosition\n }\n\n public set cursorPosition(position: number) {\n const newPosition = Math.max(0, Math.min(position, this._value.length))\n if (this._cursorPosition !== newPosition) {\n this._cursorPosition = newPosition\n this.requestRender()\n this.updateCursorPosition()\n }\n }\n\n private insertText(text: string): void {\n if (this._value.length + text.length > this._maxLength) {\n return\n }\n\n const beforeCursor = this._value.substring(0, this._cursorPosition)\n const afterCursor = this._value.substring(this._cursorPosition)\n this._value = beforeCursor + text + afterCursor\n this._cursorPosition += text.length\n this.requestRender()\n this.updateCursorPosition()\n this.emit(InputRenderableEvents.INPUT, this._value)\n }\n\n private deleteCharacter(direction: \"backward\" | \"forward\"): void {\n if (direction === \"backward\" && this._cursorPosition > 0) {\n const beforeCursor = this._value.substring(0, this._cursorPosition - 1)\n const afterCursor = this._value.substring(this._cursorPosition)\n this._value = beforeCursor + afterCursor\n this._cursorPosition--\n this.requestRender()\n this.updateCursorPosition()\n this.emit(InputRenderableEvents.INPUT, this._value)\n } else if (direction === \"forward\" && this._cursorPosition < this._value.length) {\n const beforeCursor = this._value.substring(0, this._cursorPosition)\n const afterCursor = this._value.substring(this._cursorPosition + 1)\n this._value = beforeCursor + afterCursor\n this.requestRender()\n this.updateCursorPosition()\n this.emit(InputRenderableEvents.INPUT, this._value)\n }\n }\n\n public handleKeyPress(key: ParsedKey | string): boolean {\n const keyName = typeof key === \"string\" ? key : key.name\n const keySequence = typeof key === \"string\" ? key : key.sequence\n\n switch (keyName) {\n case \"left\":\n this.cursorPosition = this._cursorPosition - 1\n return true\n\n case \"right\":\n this.cursorPosition = this._cursorPosition + 1\n return true\n\n case \"home\":\n this.cursorPosition = 0\n return true\n\n case \"end\":\n this.cursorPosition = this._value.length\n return true\n\n case \"backspace\":\n this.deleteCharacter(\"backward\")\n return true\n\n case \"delete\":\n this.deleteCharacter(\"forward\")\n return true\n\n case \"return\":\n case \"enter\":\n if (this._value !== this._lastCommittedValue) {\n this._lastCommittedValue = this._value\n this.emit(InputRenderableEvents.CHANGE, this._value)\n }\n this.emit(InputRenderableEvents.ENTER, this._value)\n return true\n\n default:\n if (\n keySequence &&\n keySequence.length === 1 &&\n keySequence.charCodeAt(0) >= 32 &&\n keySequence.charCodeAt(0) <= 126\n ) {\n this.insertText(keySequence)\n return true\n }\n break\n }\n\n return false\n }\n\n public set maxLength(maxLength: number) {\n this._maxLength = maxLength\n if (this._value.length > maxLength) {\n this._value = this._value.substring(0, maxLength)\n this.requestRender()\n }\n }\n\n public set backgroundColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n if (this._backgroundColor !== newColor) {\n this._backgroundColor = newColor\n this.requestRender()\n }\n }\n\n public set textColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.textColor)\n if (this._textColor !== newColor) {\n this._textColor = newColor\n this.requestRender()\n }\n }\n\n public set focusedBackgroundColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.focusedBackgroundColor)\n if (this._focusedBackgroundColor !== newColor) {\n this._focusedBackgroundColor = newColor\n this.requestRender()\n }\n }\n\n public set focusedTextColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.focusedTextColor)\n if (this._focusedTextColor !== newColor) {\n this._focusedTextColor = newColor\n this.requestRender()\n }\n }\n\n public set placeholderColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.placeholderColor)\n if (this._placeholderColor !== newColor) {\n this._placeholderColor = newColor\n this.requestRender()\n }\n }\n\n public set cursorColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.cursorColor)\n if (this._cursorColor !== newColor) {\n this._cursorColor = newColor\n this.requestRender()\n }\n }\n\n public updateFromLayout(): void {\n super.updateFromLayout()\n this.updateCursorPosition()\n }\n\n protected onResize(width: number, height: number): void {\n super.onResize(width, height)\n this.updateCursorPosition()\n }\n\n protected onRemove(): void {\n if (this._focused) {\n this._ctx.setCursorPosition(0, 0, false)\n }\n }\n}\n",
13
13
  "import { OptimizedBuffer } from \"../buffer\"\nimport { fonts, measureText, renderFontToFrameBuffer } from \"../lib/ascii.font\"\nimport type { ParsedKey } from \"../lib/parse.keypress\"\nimport { RGBA, parseColor, type ColorInput } from \"../lib/RGBA\"\nimport { Renderable, type RenderableOptions } from \"../Renderable\"\nimport type { RenderContext } from \"../types\"\n\nexport interface SelectOption {\n name: string\n description: string\n value?: any\n}\n\nexport interface SelectRenderableOptions extends RenderableOptions<SelectRenderable> {\n backgroundColor?: ColorInput\n textColor?: ColorInput\n focusedBackgroundColor?: ColorInput\n focusedTextColor?: ColorInput\n options?: SelectOption[]\n selectedBackgroundColor?: ColorInput\n selectedTextColor?: ColorInput\n descriptionColor?: ColorInput\n selectedDescriptionColor?: ColorInput\n showScrollIndicator?: boolean\n wrapSelection?: boolean\n showDescription?: boolean\n font?: keyof typeof fonts\n itemSpacing?: number\n fastScrollStep?: number\n}\n\nexport enum SelectRenderableEvents {\n SELECTION_CHANGED = \"selectionChanged\",\n ITEM_SELECTED = \"itemSelected\",\n}\n\nexport class SelectRenderable extends Renderable {\n protected _focusable: boolean = true\n\n private _options: SelectOption[] = []\n private selectedIndex: number = 0\n private scrollOffset: number = 0\n private maxVisibleItems: number\n\n private _backgroundColor: RGBA\n private _textColor: RGBA\n private _focusedBackgroundColor: RGBA\n private _focusedTextColor: RGBA\n private _selectedBackgroundColor: RGBA\n private _selectedTextColor: RGBA\n private _descriptionColor: RGBA\n private _selectedDescriptionColor: RGBA\n private _showScrollIndicator: boolean\n private _wrapSelection: boolean\n private _showDescription: boolean\n private _font?: keyof typeof fonts\n private _itemSpacing: number\n private linesPerItem: number\n private fontHeight: number\n private _fastScrollStep: number\n\n protected _defaultOptions = {\n backgroundColor: \"transparent\",\n textColor: \"#FFFFFF\",\n focusedBackgroundColor: \"#1a1a1a\",\n focusedTextColor: \"#FFFFFF\",\n selectedBackgroundColor: \"#334455\",\n selectedTextColor: \"#FFFF00\",\n descriptionColor: \"#888888\",\n selectedDescriptionColor: \"#CCCCCC\",\n showScrollIndicator: false,\n wrapSelection: false,\n showDescription: true,\n itemSpacing: 0,\n fastScrollStep: 5,\n } satisfies Partial<SelectRenderableOptions>\n\n constructor(ctx: RenderContext, options: SelectRenderableOptions) {\n super(ctx, { ...options, buffered: true })\n\n this._backgroundColor = parseColor(options.backgroundColor || this._defaultOptions.backgroundColor)\n this._textColor = parseColor(options.textColor || this._defaultOptions.textColor)\n this._focusedBackgroundColor = parseColor(\n options.focusedBackgroundColor || this._defaultOptions.focusedBackgroundColor,\n )\n this._focusedTextColor = parseColor(options.focusedTextColor || this._defaultOptions.focusedTextColor)\n this._options = options.options || []\n\n this._showScrollIndicator = options.showScrollIndicator ?? this._defaultOptions.showScrollIndicator\n this._wrapSelection = options.wrapSelection ?? this._defaultOptions.wrapSelection\n this._showDescription = options.showDescription ?? this._defaultOptions.showDescription\n this._font = options.font\n this._itemSpacing = options.itemSpacing || this._defaultOptions.itemSpacing\n\n this.fontHeight = this._font ? measureText({ text: \"A\", font: this._font }).height : 1\n this.linesPerItem = this._showDescription\n ? this._font\n ? this.fontHeight + 1\n : 2\n : this._font\n ? this.fontHeight\n : 1\n this.linesPerItem += this._itemSpacing\n\n this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n\n this._selectedBackgroundColor = parseColor(\n options.selectedBackgroundColor || this._defaultOptions.selectedBackgroundColor,\n )\n this._selectedTextColor = parseColor(options.selectedTextColor || this._defaultOptions.selectedTextColor)\n this._descriptionColor = parseColor(options.descriptionColor || this._defaultOptions.descriptionColor)\n this._selectedDescriptionColor = parseColor(\n options.selectedDescriptionColor || this._defaultOptions.selectedDescriptionColor,\n )\n this._fastScrollStep = options.fastScrollStep || this._defaultOptions.fastScrollStep\n\n this.requestRender() // Initial render needed\n }\n\n protected renderSelf(buffer: OptimizedBuffer, deltaTime: number): void {\n if (!this.visible || !this.frameBuffer) return\n\n if (this.isDirty) {\n this.refreshFrameBuffer()\n }\n }\n\n private refreshFrameBuffer(): void {\n if (!this.frameBuffer || this._options.length === 0) return\n\n const bgColor = this._focused ? this._focusedBackgroundColor : this._backgroundColor\n this.frameBuffer.clear(bgColor)\n\n const contentX = 0\n const contentY = 0\n const contentWidth = this.width\n const contentHeight = this.height\n\n const visibleOptions = this._options.slice(this.scrollOffset, this.scrollOffset + this.maxVisibleItems)\n\n for (let i = 0; i < visibleOptions.length; i++) {\n const actualIndex = this.scrollOffset + i\n const option = visibleOptions[i]\n const isSelected = actualIndex === this.selectedIndex\n const itemY = contentY + i * this.linesPerItem\n\n if (itemY + this.linesPerItem - 1 >= contentY + contentHeight) break\n\n if (isSelected) {\n const contentHeight = this.linesPerItem - this._itemSpacing\n this.frameBuffer.fillRect(contentX, itemY, contentWidth, contentHeight, this._selectedBackgroundColor)\n }\n\n const nameContent = `${isSelected ? \"▶ \" : \" \"}${option.name}`\n const baseTextColor = this._focused ? this._focusedTextColor : this._textColor\n const nameColor = isSelected ? this._selectedTextColor : baseTextColor\n let descX = contentX + 3\n\n if (this._font) {\n const indicator = isSelected ? \"▶ \" : \" \"\n this.frameBuffer.drawText(indicator, contentX + 1, itemY, nameColor)\n\n const indicatorWidth = 2\n renderFontToFrameBuffer(this.frameBuffer, {\n text: option.name,\n x: contentX + 1 + indicatorWidth,\n y: itemY,\n fg: nameColor,\n bg: isSelected ? this._selectedBackgroundColor : bgColor,\n font: this._font,\n })\n descX = contentX + 1 + indicatorWidth\n } else {\n this.frameBuffer.drawText(nameContent, contentX + 1, itemY, nameColor)\n }\n\n if (this._showDescription && itemY + this.fontHeight < contentY + contentHeight) {\n const descColor = isSelected ? this._selectedDescriptionColor : this._descriptionColor\n const descBg = this._focused ? this._focusedBackgroundColor : this._backgroundColor\n this.frameBuffer.drawText(option.description, descX, itemY + this.fontHeight, descColor)\n }\n }\n\n if (this._showScrollIndicator && this._options.length > this.maxVisibleItems) {\n this.renderScrollIndicatorToFrameBuffer(contentX, contentY, contentWidth, contentHeight)\n }\n }\n\n private renderScrollIndicatorToFrameBuffer(\n contentX: number,\n contentY: number,\n contentWidth: number,\n contentHeight: number,\n ): void {\n if (!this.frameBuffer) return\n\n const scrollPercent = this.selectedIndex / Math.max(1, this._options.length - 1)\n const indicatorHeight = Math.max(1, contentHeight - 2)\n const indicatorY = contentY + 1 + Math.floor(scrollPercent * indicatorHeight)\n const indicatorX = contentX + contentWidth - 1\n\n this.frameBuffer.drawText(\"█\", indicatorX, indicatorY, parseColor(\"#666666\"))\n }\n\n public get options(): SelectOption[] {\n return this._options\n }\n\n public set options(options: SelectOption[]) {\n this._options = options\n this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, options.length - 1))\n this.updateScrollOffset()\n this.requestRender()\n }\n\n public getSelectedOption(): SelectOption | null {\n return this._options[this.selectedIndex] || null\n }\n\n public getSelectedIndex(): number {\n return this.selectedIndex\n }\n\n public moveUp(steps: number = 1): void {\n const newIndex = this.selectedIndex - steps\n\n if (newIndex >= 0) {\n this.selectedIndex = newIndex\n } else if (this._wrapSelection && this._options.length > 0) {\n this.selectedIndex = this._options.length - 1\n } else {\n this.selectedIndex = 0\n }\n\n this.updateScrollOffset()\n this.requestRender()\n this.emit(SelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n }\n\n public moveDown(steps: number = 1): void {\n const newIndex = this.selectedIndex + steps\n\n if (newIndex < this._options.length) {\n this.selectedIndex = newIndex\n } else if (this._wrapSelection && this._options.length > 0) {\n this.selectedIndex = 0\n } else {\n this.selectedIndex = this._options.length - 1\n }\n\n this.updateScrollOffset()\n this.requestRender()\n this.emit(SelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n }\n\n public selectCurrent(): void {\n const selected = this.getSelectedOption()\n if (selected) {\n this.emit(SelectRenderableEvents.ITEM_SELECTED, this.selectedIndex, selected)\n }\n }\n\n public setSelectedIndex(index: number): void {\n if (index >= 0 && index < this._options.length) {\n this.selectedIndex = index\n this.updateScrollOffset()\n this.requestRender()\n this.emit(SelectRenderableEvents.SELECTION_CHANGED, this.selectedIndex, this.getSelectedOption())\n }\n }\n\n private updateScrollOffset(): void {\n if (!this._options) return\n\n const halfVisible = Math.floor(this.maxVisibleItems / 2)\n const newScrollOffset = Math.max(\n 0,\n Math.min(this.selectedIndex - halfVisible, this._options.length - this.maxVisibleItems),\n )\n\n if (newScrollOffset !== this.scrollOffset) {\n this.scrollOffset = newScrollOffset\n this.requestRender()\n }\n }\n\n protected onResize(width: number, height: number): void {\n this.maxVisibleItems = Math.max(1, Math.floor(height / this.linesPerItem))\n this.updateScrollOffset()\n this.requestRender()\n }\n\n public handleKeyPress(key: ParsedKey | string): boolean {\n const keyName = typeof key === \"string\" ? key : key.name\n const isShift = typeof key !== \"string\" && key.shift\n\n switch (keyName) {\n case \"up\":\n case \"k\":\n this.moveUp(isShift ? this._fastScrollStep : 1)\n return true\n case \"down\":\n case \"j\":\n this.moveDown(isShift ? this._fastScrollStep : 1)\n return true\n case \"return\":\n case \"enter\":\n this.selectCurrent()\n return true\n }\n\n return false\n }\n\n public get showScrollIndicator(): boolean {\n return this._showScrollIndicator\n }\n\n public set showScrollIndicator(show: boolean) {\n this._showScrollIndicator = show\n this.requestRender()\n }\n\n public get showDescription(): boolean {\n return this._showDescription\n }\n\n public set showDescription(show: boolean) {\n if (this._showDescription !== show) {\n this._showDescription = show\n this.linesPerItem = this._showDescription\n ? this._font\n ? this.fontHeight + 1\n : 2\n : this._font\n ? this.fontHeight\n : 1\n this.linesPerItem += this._itemSpacing\n\n this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n this.updateScrollOffset()\n this.requestRender()\n }\n }\n\n public get wrapSelection(): boolean {\n return this._wrapSelection\n }\n\n public set wrapSelection(wrap: boolean) {\n this._wrapSelection = wrap\n }\n\n public set backgroundColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.backgroundColor)\n if (this._backgroundColor !== newColor) {\n this._backgroundColor = newColor\n this.requestRender()\n }\n }\n\n public set textColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.textColor)\n if (this._textColor !== newColor) {\n this._textColor = newColor\n this.requestRender()\n }\n }\n\n public set focusedBackgroundColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.focusedBackgroundColor)\n if (this._focusedBackgroundColor !== newColor) {\n this._focusedBackgroundColor = newColor\n this.requestRender()\n }\n }\n\n public set focusedTextColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.focusedTextColor)\n if (this._focusedTextColor !== newColor) {\n this._focusedTextColor = newColor\n this.requestRender()\n }\n }\n\n public set selectedBackgroundColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.selectedBackgroundColor)\n if (this._selectedBackgroundColor !== newColor) {\n this._selectedBackgroundColor = newColor\n this.requestRender()\n }\n }\n\n public set selectedTextColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.selectedTextColor)\n if (this._selectedTextColor !== newColor) {\n this._selectedTextColor = newColor\n this.requestRender()\n }\n }\n\n public set descriptionColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.descriptionColor)\n if (this._descriptionColor !== newColor) {\n this._descriptionColor = newColor\n this.requestRender()\n }\n }\n\n public set selectedDescriptionColor(value: ColorInput) {\n const newColor = parseColor(value ?? this._defaultOptions.selectedDescriptionColor)\n if (this._selectedDescriptionColor !== newColor) {\n this._selectedDescriptionColor = newColor\n this.requestRender()\n }\n }\n\n public set font(font: keyof typeof fonts) {\n this._font = font\n this.fontHeight = measureText({ text: \"A\", font: this._font }).height\n this.linesPerItem = this._showDescription\n ? this._font\n ? this.fontHeight + 1\n : 2\n : this._font\n ? this.fontHeight\n : 1\n this.linesPerItem += this._itemSpacing\n this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n this.updateScrollOffset()\n this.requestRender()\n }\n\n public set itemSpacing(spacing: number) {\n this._itemSpacing = spacing\n this.linesPerItem = this._showDescription\n ? this._font\n ? this.fontHeight + 1\n : 2\n : this._font\n ? this.fontHeight\n : 1\n this.linesPerItem += this._itemSpacing\n this.maxVisibleItems = Math.max(1, Math.floor(this.height / this.linesPerItem))\n this.updateScrollOffset()\n this.requestRender()\n }\n\n public set fastScrollStep(step: number) {\n this._fastScrollStep = step\n }\n}\n",
@@ -9,6 +9,7 @@ type KeyHandlerEventMap = {
9
9
  export declare class KeyHandler extends EventEmitter<KeyHandlerEventMap> {
10
10
  private stdin;
11
11
  private useKittyKeyboard;
12
+ private listener;
12
13
  constructor(stdin?: NodeJS.ReadStream, useKittyKeyboard?: boolean);
13
14
  destroy(): void;
14
15
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "type": "module",
7
- "version": "0.0.0-20250922-6d7f4921",
7
+ "version": "0.0.0-20250922-2a20774e",
8
8
  "description": "OpenTUI is a TypeScript library for building terminal user interfaces (TUIs)",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -40,11 +40,11 @@
40
40
  "bun-webgpu": "0.1.3",
41
41
  "planck": "^1.4.2",
42
42
  "three": "0.177.0",
43
- "@opentui/core-darwin-x64": "0.0.0-20250922-6d7f4921",
44
- "@opentui/core-darwin-arm64": "0.0.0-20250922-6d7f4921",
45
- "@opentui/core-linux-x64": "0.0.0-20250922-6d7f4921",
46
- "@opentui/core-linux-arm64": "0.0.0-20250922-6d7f4921",
47
- "@opentui/core-win32-x64": "0.0.0-20250922-6d7f4921",
48
- "@opentui/core-win32-arm64": "0.0.0-20250922-6d7f4921"
43
+ "@opentui/core-darwin-x64": "0.0.0-20250922-2a20774e",
44
+ "@opentui/core-darwin-arm64": "0.0.0-20250922-2a20774e",
45
+ "@opentui/core-linux-x64": "0.0.0-20250922-2a20774e",
46
+ "@opentui/core-linux-arm64": "0.0.0-20250922-2a20774e",
47
+ "@opentui/core-win32-x64": "0.0.0-20250922-2a20774e",
48
+ "@opentui/core-win32-arm64": "0.0.0-20250922-2a20774e"
49
49
  }
50
50
  }
package/renderer.d.ts CHANGED
@@ -141,6 +141,7 @@ export declare class CliRenderer extends EventEmitter implements RenderContext {
141
141
  private _currentFocusedRenderable;
142
142
  private lifecyclePasses;
143
143
  private handleError;
144
+ private dumpOutputCache;
144
145
  private exitHandler;
145
146
  private warningHandler;
146
147
  constructor(lib: RenderLib, rendererPtr: Pointer, stdin: NodeJS.ReadStream, stdout: NodeJS.WriteStream, width: number, height: number, config?: CliRendererConfig);
package/testing.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  CliRenderer,
4
4
  resolveRenderLib
5
- } from "./index-ra8j4k81.js";
5
+ } from "./index-mtc20a8y.js";
6
6
 
7
7
  // src/testing/mock-keys.ts
8
8
  var KeyCodes = {