@mariozechner/pi-tui 0.7.20 → 0.7.22

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.
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/components/text.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport type { Component } from \"../tui.js\";\nimport { visibleWidth, wrapTextWithAnsi } from \"../utils.js\";\n\n/**\n * Text component - displays multi-line text with word wrapping\n */\nexport class Text implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate customBgRgb?: { r: number; g: number; b: number };\n\n\t// Cache for rendered output\n\tprivate cachedText?: string;\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\n\tconstructor(\n\t\ttext: string = \"\",\n\t\tpaddingX: number = 1,\n\t\tpaddingY: number = 1,\n\t\tcustomBgRgb?: { r: number; g: number; b: number },\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.customBgRgb = customBgRgb;\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\t// Invalidate cache when text changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tsetCustomBgRgb(customBgRgb?: { r: number; g: number; b: number }): void {\n\t\tthis.customBgRgb = customBgRgb;\n\t\t// Invalidate cache when color changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Check cache\n\t\tif (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\t// Calculate available width for content (subtract horizontal padding)\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\n\t\t// Don't render anything if there's no actual text\n\t\tif (!this.text || this.text.trim() === \"\") {\n\t\t\tconst result: string[] = [];\n\t\t\t// Update cache\n\t\t\tthis.cachedText = this.text;\n\t\t\tthis.cachedWidth = width;\n\t\t\tthis.cachedLines = result;\n\t\t\treturn result;\n\t\t}\n\n\t\t// Replace tabs with 3 spaces for consistent rendering\n\t\tconst normalizedText = this.text.replace(/\\t/g, \" \");\n\n\t\t// Use shared ANSI-aware word wrapping\n\t\tconst lines = wrapTextWithAnsi(normalizedText, contentWidth);\n\n\t\t// Add padding to each line\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\t\tconst paddedLines: string[] = [];\n\n\t\tfor (const line of lines) {\n\t\t\t// Calculate visible length (strip ANSI codes)\n\t\t\tconst visibleLength = visibleWidth(line);\n\t\t\t// Right padding to fill to width (accounting for left padding and content)\n\t\t\tconst rightPadLength = Math.max(0, width - this.paddingX - visibleLength);\n\t\t\tconst rightPad = \" \".repeat(rightPadLength);\n\t\t\tlet paddedLine = leftPad + line + rightPad;\n\n\t\t\t// Apply background color if specified\n\t\t\tif (this.customBgRgb) {\n\t\t\t\tpaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(paddedLine);\n\t\t\t}\n\n\t\t\tpaddedLines.push(paddedLine);\n\t\t}\n\n\t\t// Add top padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst topPadding: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tlet emptyPaddedLine = emptyLine;\n\t\t\tif (this.customBgRgb) {\n\t\t\t\temptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);\n\t\t\t}\n\t\t\ttopPadding.push(emptyPaddedLine);\n\t\t}\n\n\t\t// Add bottom padding (empty lines)\n\t\tconst bottomPadding: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tlet emptyPaddedLine = emptyLine;\n\t\t\tif (this.customBgRgb) {\n\t\t\t\temptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);\n\t\t\t}\n\t\t\tbottomPadding.push(emptyPaddedLine);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = [...topPadding, ...paddedLines, ...bottomPadding];\n\n\t\t// Update cache\n\t\tthis.cachedText = this.text;\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = result;\n\n\t\treturn result.length > 0 ? result : [\"\"];\n\t}\n}\n"]}
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/components/text.ts"],"names":[],"mappings":"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","sourcesContent":["import { Chalk } from \"chalk\";\nimport type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.js\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Text component - displays multi-line text with word wrapping\n */\nexport class Text implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate customBgRgb?: { r: number; g: number; b: number };\n\n\t// Cache for rendered output\n\tprivate cachedText?: string;\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\n\tconstructor(\n\t\ttext: string = \"\",\n\t\tpaddingX: number = 1,\n\t\tpaddingY: number = 1,\n\t\tcustomBgRgb?: { r: number; g: number; b: number },\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.customBgRgb = customBgRgb;\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tsetCustomBgRgb(customBgRgb?: { r: number; g: number; b: number }): void {\n\t\tthis.customBgRgb = customBgRgb;\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Check cache\n\t\tif (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\t// Don't render anything if there's no actual text\n\t\tif (!this.text || this.text.trim() === \"\") {\n\t\t\tconst result: string[] = [];\n\t\t\tthis.cachedText = this.text;\n\t\t\tthis.cachedWidth = width;\n\t\t\tthis.cachedLines = result;\n\t\t\treturn result;\n\t\t}\n\n\t\t// Replace tabs with 3 spaces\n\t\tconst normalizedText = this.text.replace(/\\t/g, \" \");\n\n\t\t// Calculate content width (subtract left/right margins)\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\n\t\t// Wrap text (this preserves ANSI codes but does NOT pad)\n\t\tconst wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth);\n\n\t\t// Add margins and background to each line\n\t\tconst leftMargin = \" \".repeat(this.paddingX);\n\t\tconst rightMargin = \" \".repeat(this.paddingX);\n\t\tconst contentLines: string[] = [];\n\n\t\tfor (const line of wrappedLines) {\n\t\t\t// Add margins\n\t\t\tconst lineWithMargins = leftMargin + line + rightMargin;\n\n\t\t\t// Apply background if specified (this also pads to full width)\n\t\t\tif (this.customBgRgb) {\n\t\t\t\tcontentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgRgb));\n\t\t\t} else {\n\t\t\t\t// No background - just pad to width with spaces\n\t\t\t\tconst visibleLen = visibleWidth(lineWithMargins);\n\t\t\t\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\t\t\t\tcontentLines.push(lineWithMargins + \" \".repeat(paddingNeeded));\n\t\t\t}\n\t\t}\n\n\t\t// Add top/bottom padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst emptyLines: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tconst line = this.customBgRgb ? applyBackgroundToLine(emptyLine, width, this.customBgRgb) : emptyLine;\n\t\t\temptyLines.push(line);\n\t\t}\n\n\t\tconst result = [...emptyLines, ...contentLines, ...emptyLines];\n\n\t\t// Update cache\n\t\tthis.cachedText = this.text;\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = result;\n\n\t\treturn result.length > 0 ? result : [\"\"];\n\t}\n}\n"]}
package/dist/tui.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEhC;;OAEG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,qBAAa,SAAU,YAAW,SAAS;IAC1C,QAAQ,EAAE,SAAS,EAAE,CAAM;IAE3B,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAKtC;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9B;CACD;AAED;;GAEG;AACH,qBAAa,GAAI,SAAQ,SAAS;IACjC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAK;IAEtB,YAAY,QAAQ,EAAE,QAAQ,EAG7B;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAE1C;IAED,KAAK,IAAI,IAAI,CAOZ;IAED,IAAI,IAAI,IAAI,CAGX;IAED,aAAa,IAAI,IAAI,CAOpB;IAED,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,QAAQ;CAwHhB","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport type { Terminal } from \"./terminal.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tprivate terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.requestRender();\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(): void {\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tlet lastChanged = -1;\n\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t\tlastChanged = i;\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\t\tbuffer += \"\\x1b[J\"; // Clear from cursor to end of screen\n\n\t\t// Render from first changed line to end\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tif (visibleWidth(newLines[i]) > width) {\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width\\n\\n${newLines[i]}`);\n\t\t\t}\n\t\t\tbuffer += newLines[i];\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEhC;;OAEG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,qBAAa,SAAU,YAAW,SAAS;IAC1C,QAAQ,EAAE,SAAS,EAAE,CAAM;IAE3B,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAKtC;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9B;CACD;AAED;;GAEG;AACH,qBAAa,GAAI,SAAQ,SAAS;IACjC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,gBAAgB,CAA0B;IAClD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAK;IAEtB,YAAY,QAAQ,EAAE,QAAQ,EAG7B;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAE1C;IAED,KAAK,IAAI,IAAI,CAOZ;IAED,IAAI,IAAI,IAAI,CAGX;IAED,aAAa,IAAI,IAAI,CAOpB;IAED,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,QAAQ;CAmIhB","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport type { Terminal } from \"./terminal.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tprivate terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.requestRender();\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(): void {\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tlet lastChanged = -1;\n\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t\tlastChanged = i;\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tif (visibleWidth(newLines[i]) > width) {\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width\\n\\n${newLines[i]}`);\n\t\t\t}\n\t\t\tbuffer += newLines[i];\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
package/dist/tui.js CHANGED
@@ -161,16 +161,26 @@ export class TUI extends Container {
161
161
  buffer += `\x1b[${-lineDiff}A`; // Move up
162
162
  }
163
163
  buffer += "\r"; // Move to column 0
164
- buffer += "\x1b[J"; // Clear from cursor to end of screen
165
- // Render from first changed line to end
164
+ // Render from first changed line to end, clearing each line before writing
165
+ // This avoids the \x1b[J clear-to-end which can cause flicker in xterm.js
166
166
  for (let i = firstChanged; i < newLines.length; i++) {
167
167
  if (i > firstChanged)
168
168
  buffer += "\r\n";
169
+ buffer += "\x1b[2K"; // Clear current line
169
170
  if (visibleWidth(newLines[i]) > width) {
170
171
  throw new Error(`Rendered line ${i} exceeds terminal width\n\n${newLines[i]}`);
171
172
  }
172
173
  buffer += newLines[i];
173
174
  }
175
+ // If we had more lines before, clear them and move cursor back
176
+ if (this.previousLines.length > newLines.length) {
177
+ const extraLines = this.previousLines.length - newLines.length;
178
+ for (let i = newLines.length; i < this.previousLines.length; i++) {
179
+ buffer += "\r\n\x1b[2K";
180
+ }
181
+ // Move cursor back to end of new content
182
+ buffer += `\x1b[${extraLines}A`;
183
+ }
174
184
  buffer += "\x1b[?2026l"; // End synchronized output
175
185
  // Write entire buffer at once
176
186
  this.terminal.write(buffer);
package/dist/tui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport type { Terminal } from \"./terminal.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tprivate terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.requestRender();\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(): void {\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tlet lastChanged = -1;\n\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t\tlastChanged = i;\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\t\tbuffer += \"\\x1b[J\"; // Clear from cursor to end of screen\n\n\t\t// Render from first changed line to end\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tif (visibleWidth(newLines[i]) > width) {\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width\\n\\n${newLines[i]}`);\n\t\t\t}\n\t\t\tbuffer += newLines[i];\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
1
+ {"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport type { Terminal } from \"./terminal.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tprivate terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.requestRender();\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(): void {\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tlet lastChanged = -1;\n\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t\tlastChanged = i;\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tif (visibleWidth(newLines[i]) > width) {\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width\\n\\n${newLines[i]}`);\n\t\t\t}\n\t\t\tbuffer += newLines[i];\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
package/dist/utils.d.ts CHANGED
@@ -1,23 +1,33 @@
1
1
  /**
2
2
  * Calculate the visible width of a string in terminal columns.
3
- * This correctly handles:
4
- * - ANSI escape codes (ignored)
5
- * - Emojis and wide characters (counted as 2 columns)
6
- * - Combining characters (counted correctly)
7
- * - Tabs (replaced with 3 spaces for consistent width)
8
3
  */
9
4
  export declare function visibleWidth(str: string): number;
10
5
  /**
11
- * Wrap text lines with word-based wrapping while preserving ANSI escape codes.
12
- * This function properly handles:
13
- * - ANSI escape codes (preserved and tracked across lines)
14
- * - Word-based wrapping (breaks at spaces when possible)
15
- * - Multi-byte characters (emoji, surrogate pairs)
16
- * - Newlines within text
6
+ * Wrap text with ANSI codes preserved.
17
7
  *
18
- * @param text - The text to wrap (can contain ANSI codes and newlines)
19
- * @param width - The maximum width in terminal columns
20
- * @returns Array of wrapped lines with ANSI codes preserved
8
+ * ONLY does word wrapping - NO padding, NO background colors.
9
+ * Returns lines where each line is <= width visible chars.
10
+ * Active ANSI codes are preserved across line breaks.
11
+ *
12
+ * @param text - Text to wrap (may contain ANSI codes and newlines)
13
+ * @param width - Maximum visible width per line
14
+ * @returns Array of wrapped lines (NOT padded to width)
21
15
  */
22
16
  export declare function wrapTextWithAnsi(text: string, width: number): string[];
17
+ /**
18
+ * Apply background color to a line, padding to full width.
19
+ *
20
+ * Handles the tricky case where content contains \x1b[0m resets that would
21
+ * kill the background color. We reapply the background after any reset.
22
+ *
23
+ * @param line - Line of text (may contain ANSI codes)
24
+ * @param width - Total width to pad to
25
+ * @param bgRgb - Background RGB color
26
+ * @returns Line with background applied and padded to width
27
+ */
28
+ export declare function applyBackgroundToLine(line: string, width: number, bgRgb: {
29
+ r: number;
30
+ g: number;
31
+ b: number;
32
+ }): string;
23
33
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD;AAwED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n * This correctly handles:\n * - ANSI escape codes (ignored)\n * - Emojis and wide characters (counted as 2 columns)\n * - Combining characters (counted correctly)\n * - Tabs (replaced with 3 spaces for consistent width)\n */\nexport function visibleWidth(str: string): number {\n\t// Replace tabs with 3 spaces before measuring\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n * Returns the ANSI code and the length consumed, or null if no ANSI code found.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track and manage active ANSI codes for preserving styling across wrapped lines.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\t/**\n\t * Process an ANSI code and update the active codes.\n\t */\n\tprocess(ansiCode: string): void {\n\t\t// Check if it's a styling code (ends with 'm')\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Reset code clears all active codes\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\t// Add to active codes\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\t/**\n\t * Get all active ANSI codes as a single string.\n\t */\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\t/**\n\t * Check if there are any active codes.\n\t */\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n\n\t/**\n\t * Get the reset code.\n\t */\n\tgetResetCode(): string {\n\t\treturn \"\\x1b[0m\";\n\t}\n}\n\n/**\n * Wrap text lines with word-based wrapping while preserving ANSI escape codes.\n * This function properly handles:\n * - ANSI escape codes (preserved and tracked across lines)\n * - Word-based wrapping (breaks at spaces when possible)\n * - Multi-byte characters (emoji, surrogate pairs)\n * - Newlines within text\n *\n * @param text - The text to wrap (can contain ANSI codes and newlines)\n * @param width - The maximum width in terminal columns\n * @returns Array of wrapped lines with ANSI codes preserved\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLineWithAnsi(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\n/**\n * Wrap a single line (no newlines) with word-based wrapping while preserving ANSI codes.\n */\nfunction wrapSingleLineWithAnsi(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\n\t// First, split the line into words while preserving ANSI codes with their words\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// If the word itself is longer than the width, we need to break it character by character\n\t\tif (wordVisibleLength > width) {\n\t\t\t// Flush current line if any\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(closeLineAndPrepareNext(currentLine, tracker));\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break the long word\n\t\t\tconst brokenLines = breakLongWordWithAnsi(word, width, tracker);\n\t\t\twrapped.push(...brokenLines.slice(0, -1));\n\t\t\tcurrentLine = brokenLines[brokenLines.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t} else {\n\t\t\t// Check if adding this word would exceed the width\n\t\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0; // Space before word if not at line start\n\t\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\t\tif (totalNeeded > width) {\n\t\t\t\t// Word doesn't fit, wrap to next line\n\t\t\t\tif (currentLine) {\n\t\t\t\t\twrapped.push(closeLineAndPrepareNext(currentLine, tracker));\n\t\t\t\t}\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t} else {\n\t\t\t\t// Word fits, add it\n\t\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t\t} else {\n\t\t\t\t\tcurrentLine += word;\n\t\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Update tracker with ANSI codes from this word\n\t\t\tupdateTrackerFromText(word, tracker);\n\t\t}\n\t}\n\n\t// Add final line\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n/**\n * Close current line with reset code if needed, and prepare the next line with active codes.\n */\nfunction closeLineAndPrepareNext(line: string, tracker: AnsiCodeTracker): string {\n\tif (tracker.hasActiveCodes()) {\n\t\treturn line + tracker.getResetCode();\n\t}\n\treturn line;\n}\n\n/**\n * Update the ANSI code tracker by scanning through text.\n */\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached to their words.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\t// Check for ANSI code\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check for space (word boundary)\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Regular character\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\t// Add final word\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Break a long word that doesn't fit on a single line, character by character.\n */\nfunction breakLongWordWithAnsi(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentVisibleLength = 0;\n\tlet i = 0;\n\n\twhile (i < word.length) {\n\t\t// Check for ANSI code\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentLine += ansiResult.code;\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Get character (handle surrogate pairs)\n\t\tconst codePoint = word.charCodeAt(i);\n\t\tlet char: string;\n\t\tlet charByteLength: number;\n\n\t\tif (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < word.length) {\n\t\t\t// High surrogate - get the pair\n\t\t\tchar = word.substring(i, i + 2);\n\t\t\tcharByteLength = 2;\n\t\t} else {\n\t\t\t// Regular character\n\t\t\tchar = word[i];\n\t\t\tcharByteLength = 1;\n\t\t}\n\n\t\tconst charWidth = visibleWidth(char);\n\n\t\t// Check if adding this character would exceed width\n\t\tif (currentVisibleLength + charWidth > width) {\n\t\t\t// Need to wrap\n\t\t\tif (tracker.hasActiveCodes()) {\n\t\t\t\tlines.push(currentLine + tracker.getResetCode());\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t} else {\n\t\t\t\tlines.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t}\n\t\t\tcurrentVisibleLength = 0;\n\t\t}\n\n\t\tcurrentLine += char;\n\t\tcurrentVisibleLength += charWidth;\n\t\ti += charByteLength;\n\t}\n\n\t// Add final line (don't close it, let the caller handle that)\n\tif (currentLine || lines.length === 0) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines;\n}\n"]}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAKA;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAkBrH","sourcesContent":["import { Chalk } from \"chalk\";\nimport stringWidth from \"string-width\";\n\nconst colorChalk = new Chalk({ level: 3 });\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoWordsWithAnsi(text: string): string[] {\n\tconst words: string[] = [];\n\tlet currentWord = \"\";\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst char = text[i];\n\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentWord += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === \" \") {\n\t\t\tif (currentWord) {\n\t\t\t\twords.push(currentWord);\n\t\t\t\tcurrentWord = \"\";\n\t\t\t}\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcurrentWord += char;\n\t\ti++;\n\t}\n\n\tif (currentWord) {\n\t\twords.push(currentWord);\n\t}\n\n\treturn words;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst words = splitIntoWordsWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const word of words) {\n\t\tconst wordVisibleLength = visibleWidth(word);\n\n\t\t// Word itself is too long - break it character by character\n\t\tif (wordVisibleLength > width) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long word\n\t\t\tconst broken = breakLongWord(word, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this word would exceed width\n\t\tconst spaceNeeded = currentVisibleLength > 0 ? 1 : 0;\n\t\tconst totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line\n\t\t\twrapped.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes() + word;\n\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tif (currentVisibleLength > 0) {\n\t\t\t\tcurrentLine += \" \" + word;\n\t\t\t\tcurrentVisibleLength += 1 + wordVisibleLength;\n\t\t\t} else {\n\t\t\t\tcurrentLine += word;\n\t\t\t\tcurrentVisibleLength = wordVisibleLength;\n\t\t\t}\n\t\t}\n\n\t\tupdateTrackerFromText(word, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\tlet i = 0;\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tcurrentLine += ansiResult.code;\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = word[i];\n\t\tconst charWidth = visibleWidth(char);\n\n\t\tif (currentWidth + charWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += char;\n\t\tcurrentWidth += charWidth;\n\t\ti++;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * Handles the tricky case where content contains \\x1b[0m resets that would\n * kill the background color. We reapply the background after any reset.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgRgb - Background RGB color\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgRgb: { r: number; g: number; b: number }): string {\n\tconst bgStart = `\\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;\n\tconst bgEnd = \"\\x1b[49m\";\n\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Strategy: wrap content + padding in background, then fix any 0m resets\n\tconst withPadding = line + padding;\n\tconst withBg = bgStart + withPadding + bgEnd;\n\n\t// Find all \\x1b[0m or \\x1b[49m that would kill background\n\t// Replace with reset + background reapplication\n\tconst fixedBg = withBg.replace(/\\x1b\\[0m/g, `\\x1b[0m${bgStart}`);\n\n\treturn fixedBg;\n}\n"]}