@mariozechner/pi-tui 0.7.18 → 0.7.21
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/dist/components/markdown.d.ts +4 -5
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +47 -157
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +27 -82
- package/dist/components/text.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +12 -2
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +28 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +180 -6
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/components/text.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/components/text.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAK3C;;GAEG;AACH,qBAAa,IAAK,YAAW,SAAS;IACrC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAC,CAAsC;IAG1D,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAW;IAE/B,YACC,IAAI,GAAE,MAAW,EACjB,QAAQ,GAAE,MAAU,EACpB,QAAQ,GAAE,MAAU,EACpB,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAMjD;IAED,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK1B;IAED,cAAc,CAAC,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAKtE;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA4D9B;CACD","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/components/text.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { visibleWidth } from "../utils.js";
|
|
1
|
+
import { Chalk } from "chalk";
|
|
2
|
+
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
3
|
+
const colorChalk = new Chalk({ level: 3 });
|
|
3
4
|
/**
|
|
4
5
|
* Text component - displays multi-line text with word wrapping
|
|
5
6
|
*/
|
|
@@ -20,14 +21,12 @@ export class Text {
|
|
|
20
21
|
}
|
|
21
22
|
setText(text) {
|
|
22
23
|
this.text = text;
|
|
23
|
-
// Invalidate cache when text changes
|
|
24
24
|
this.cachedText = undefined;
|
|
25
25
|
this.cachedWidth = undefined;
|
|
26
26
|
this.cachedLines = undefined;
|
|
27
27
|
}
|
|
28
28
|
setCustomBgRgb(customBgRgb) {
|
|
29
29
|
this.customBgRgb = customBgRgb;
|
|
30
|
-
// Invalidate cache when color changes
|
|
31
30
|
this.cachedText = undefined;
|
|
32
31
|
this.cachedWidth = undefined;
|
|
33
32
|
this.cachedLines = undefined;
|
|
@@ -37,100 +36,46 @@ export class Text {
|
|
|
37
36
|
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
|
|
38
37
|
return this.cachedLines;
|
|
39
38
|
}
|
|
40
|
-
// Calculate available width for content (subtract horizontal padding)
|
|
41
|
-
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
42
39
|
// Don't render anything if there's no actual text
|
|
43
40
|
if (!this.text || this.text.trim() === "") {
|
|
44
41
|
const result = [];
|
|
45
|
-
// Update cache
|
|
46
42
|
this.cachedText = this.text;
|
|
47
43
|
this.cachedWidth = width;
|
|
48
44
|
this.cachedLines = result;
|
|
49
45
|
return result;
|
|
50
46
|
}
|
|
51
|
-
// Replace tabs with 3 spaces
|
|
47
|
+
// Replace tabs with 3 spaces
|
|
52
48
|
const normalizedText = this.text.replace(/\t/g, " ");
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
// Calculate content width (subtract left/right margins)
|
|
50
|
+
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
51
|
+
// Wrap text (this preserves ANSI codes but does NOT pad)
|
|
52
|
+
const wrappedLines = wrapTextWithAnsi(normalizedText, contentWidth);
|
|
53
|
+
// Add margins and background to each line
|
|
54
|
+
const leftMargin = " ".repeat(this.paddingX);
|
|
55
|
+
const rightMargin = " ".repeat(this.paddingX);
|
|
56
|
+
const contentLines = [];
|
|
57
|
+
for (const line of wrappedLines) {
|
|
58
|
+
// Add margins
|
|
59
|
+
const lineWithMargins = leftMargin + line + rightMargin;
|
|
60
|
+
// Apply background if specified (this also pads to full width)
|
|
61
|
+
if (this.customBgRgb) {
|
|
62
|
+
contentLines.push(applyBackgroundToLine(lineWithMargins, width, this.customBgRgb));
|
|
60
63
|
}
|
|
61
64
|
else {
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const currentVisible = visibleWidth(currentLine);
|
|
67
|
-
const wordVisible = visibleWidth(word);
|
|
68
|
-
// If word is too long, truncate it
|
|
69
|
-
let finalWord = word;
|
|
70
|
-
if (wordVisible > contentWidth) {
|
|
71
|
-
// Truncate word to fit
|
|
72
|
-
let truncated = "";
|
|
73
|
-
for (const char of word) {
|
|
74
|
-
if (visibleWidth(truncated + char) > contentWidth) {
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
truncated += char;
|
|
78
|
-
}
|
|
79
|
-
finalWord = truncated;
|
|
80
|
-
}
|
|
81
|
-
if (currentVisible === 0) {
|
|
82
|
-
currentLine = finalWord;
|
|
83
|
-
}
|
|
84
|
-
else if (currentVisible + 1 + visibleWidth(finalWord) <= contentWidth) {
|
|
85
|
-
currentLine += " " + finalWord;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
lines.push(currentLine);
|
|
89
|
-
currentLine = finalWord;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (currentLine.length > 0) {
|
|
93
|
-
lines.push(currentLine);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
// Add padding to each line
|
|
98
|
-
const leftPad = " ".repeat(this.paddingX);
|
|
99
|
-
const paddedLines = [];
|
|
100
|
-
for (const line of lines) {
|
|
101
|
-
// Calculate visible length (strip ANSI codes)
|
|
102
|
-
const visibleLength = visibleWidth(line);
|
|
103
|
-
// Right padding to fill to width (accounting for left padding and content)
|
|
104
|
-
const rightPadLength = Math.max(0, width - this.paddingX - visibleLength);
|
|
105
|
-
const rightPad = " ".repeat(rightPadLength);
|
|
106
|
-
let paddedLine = leftPad + line + rightPad;
|
|
107
|
-
// Apply background color if specified
|
|
108
|
-
if (this.customBgRgb) {
|
|
109
|
-
paddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(paddedLine);
|
|
65
|
+
// No background - just pad to width with spaces
|
|
66
|
+
const visibleLen = visibleWidth(lineWithMargins);
|
|
67
|
+
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
68
|
+
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
110
69
|
}
|
|
111
|
-
paddedLines.push(paddedLine);
|
|
112
70
|
}
|
|
113
|
-
// Add top padding (empty lines)
|
|
71
|
+
// Add top/bottom padding (empty lines)
|
|
114
72
|
const emptyLine = " ".repeat(width);
|
|
115
|
-
const
|
|
73
|
+
const emptyLines = [];
|
|
116
74
|
for (let i = 0; i < this.paddingY; i++) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);
|
|
120
|
-
}
|
|
121
|
-
topPadding.push(emptyPaddedLine);
|
|
122
|
-
}
|
|
123
|
-
// Add bottom padding (empty lines)
|
|
124
|
-
const bottomPadding = [];
|
|
125
|
-
for (let i = 0; i < this.paddingY; i++) {
|
|
126
|
-
let emptyPaddedLine = emptyLine;
|
|
127
|
-
if (this.customBgRgb) {
|
|
128
|
-
emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);
|
|
129
|
-
}
|
|
130
|
-
bottomPadding.push(emptyPaddedLine);
|
|
75
|
+
const line = this.customBgRgb ? applyBackgroundToLine(emptyLine, width, this.customBgRgb) : emptyLine;
|
|
76
|
+
emptyLines.push(line);
|
|
131
77
|
}
|
|
132
|
-
|
|
133
|
-
const result = [...topPadding, ...paddedLines, ...bottomPadding];
|
|
78
|
+
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
|
134
79
|
// Update cache
|
|
135
80
|
this.cachedText = this.text;
|
|
136
81
|
this.cachedWidth = width;
|
|
@@ -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 } 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\tconst lines: string[] = [];\n\t\tconst textLines = normalizedText.split(\"\\n\");\n\n\t\tfor (const line of textLines) {\n\t\t\t// Measure visible length (strip ANSI codes)\n\t\t\tconst visibleLineLength = visibleWidth(line);\n\n\t\t\tif (visibleLineLength <= contentWidth) {\n\t\t\t\tlines.push(line);\n\t\t\t} else {\n\t\t\t\t// Word wrap\n\t\t\t\tconst words = line.split(\" \");\n\t\t\t\tlet currentLine = \"\";\n\n\t\t\t\tfor (const word of words) {\n\t\t\t\t\tconst currentVisible = visibleWidth(currentLine);\n\t\t\t\t\tconst wordVisible = visibleWidth(word);\n\n\t\t\t\t\t// If word is too long, truncate it\n\t\t\t\t\tlet finalWord = word;\n\t\t\t\t\tif (wordVisible > contentWidth) {\n\t\t\t\t\t\t// Truncate word to fit\n\t\t\t\t\t\tlet truncated = \"\";\n\t\t\t\t\t\tfor (const char of word) {\n\t\t\t\t\t\t\tif (visibleWidth(truncated + char) > contentWidth) {\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\ttruncated += char;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfinalWord = truncated;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (currentVisible === 0) {\n\t\t\t\t\t\tcurrentLine = finalWord;\n\t\t\t\t\t} else if (currentVisible + 1 + visibleWidth(finalWord) <= contentWidth) {\n\t\t\t\t\t\tcurrentLine += \" \" + finalWord;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlines.push(currentLine);\n\t\t\t\t\t\tcurrentLine = finalWord;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (currentLine.length > 0) {\n\t\t\t\t\tlines.push(currentLine);\n\t\t\t\t}\n\t\t\t}\n\t\t}\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;
|
|
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
|
-
|
|
165
|
-
//
|
|
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":"AAAA;;GAEG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAmB1C,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,MAAM,OAAO,SAAS;IACrB,QAAQ,GAAgB,EAAE,CAAC;IAE3B,QAAQ,CAAC,SAAoB,EAAQ;QACpC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAAA,CAC9B;IAED,WAAW,CAAC,SAAoB,EAAQ;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAChC,CAAC;IAAA,CACD;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IAAA,CACnB;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,KAAK,CAAC;IAAA,CACb;CACD;AAED;;GAEG;AACH,MAAM,OAAO,GAAI,SAAQ,SAAS;IACzB,QAAQ,CAAW;IACnB,aAAa,GAAa,EAAE,CAAC;IAC7B,aAAa,GAAG,CAAC,CAAC;IAClB,gBAAgB,GAAqB,IAAI,CAAC;IAC1C,eAAe,GAAG,KAAK,CAAC;IACxB,SAAS,GAAG,CAAC,CAAC,CAAC,gEAAgE;IAEvF,YAAY,QAAkB,EAAE;QAC/B,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAAA,CACzB;IAED,QAAQ,CAAC,SAA2B,EAAQ;QAC3C,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAAA,CAClC;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,CAAC,KAAK,CAClB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAChC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,CAC1B,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAED,IAAI,GAAS;QACZ,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAAA,CACrB;IAED,aAAa,GAAS;QACrB,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO;QACjC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAAA,CAChB,CAAC,CAAC;IAAA,CACH;IAEO,WAAW,CAAC,IAAY,EAAQ;QACvC,qDAAqD;QACrD,wDAAwD;QACxD,IAAI,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,CAAC,aAAa,EAAE,CAAC;QACtB,CAAC;IAAA,CACD;IAEO,QAAQ,GAAS;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAElC,yCAAyC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEpC,sCAAsC;QACtC,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,KAAK,KAAK,CAAC;QAE9E,yDAAyD;QACzD,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,oEAAoE;YACpE,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,iCAAiC;QACjC,IAAI,YAAY,EAAE,CAAC;YAClB,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,oCAAoC;QACpC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;QACtB,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC;QAErB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAEvD,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;gBACzB,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;oBACzB,YAAY,GAAG,CAAC,CAAC;gBAClB,CAAC;gBACD,WAAW,GAAG,CAAC,CAAC;YACjB,CAAC;QACF,CAAC;QAED,aAAa;QACb,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;YACzB,OAAO;QACR,CAAC;QAED,gDAAgD;QAChD,oDAAoD;QACpD,kEAAkE;QAClE,wDAAwD;QACxD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC;QAChD,IAAI,YAAY,GAAG,WAAW,EAAE,CAAC;YAChC,uDAAuD;YACvD,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,wCAAwC;QACxC,+DAA+D;QAC/D,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;QAExD,oCAAoC;QACpC,MAAM,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/C,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,QAAQ,QAAQ,GAAG,CAAC,CAAC,YAAY;QAC5C,CAAC;aAAM,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,QAAQ,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU;QAC3C,CAAC;QAED,MAAM,IAAI,IAAI,CAAC,CAAC,mBAAmB;QACnC,MAAM,IAAI,QAAQ,CAAC,CAAC,qCAAqC;QAEzD,wCAAwC;QACxC,KAAK,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC,GAAG,YAAY;gBAAE,MAAM,IAAI,MAAM,CAAC;YACvC,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC;gBACvC,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,8BAA8B,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAChF,CAAC;YACD,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;QACvB,CAAC;QAED,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;QAEnD,8BAA8B;QAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAE5B,oCAAoC;QACpC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAErC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;QAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAAA,CAC3B;CACD","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,10 +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;
|
|
5
|
+
/**
|
|
6
|
+
* Wrap text with ANSI codes preserved.
|
|
7
|
+
*
|
|
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)
|
|
15
|
+
*/
|
|
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;
|
|
10
33
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"
|
|
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;AAoDD;;;;;;;;;;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// 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\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"]}
|
package/dist/utils.js
CHANGED
|
@@ -1,15 +1,189 @@
|
|
|
1
|
+
import { Chalk } from "chalk";
|
|
1
2
|
import stringWidth from "string-width";
|
|
3
|
+
const colorChalk = new Chalk({ level: 3 });
|
|
2
4
|
/**
|
|
3
5
|
* Calculate the visible width of a string in terminal columns.
|
|
4
|
-
* This correctly handles:
|
|
5
|
-
* - ANSI escape codes (ignored)
|
|
6
|
-
* - Emojis and wide characters (counted as 2 columns)
|
|
7
|
-
* - Combining characters (counted correctly)
|
|
8
|
-
* - Tabs (replaced with 3 spaces for consistent width)
|
|
9
6
|
*/
|
|
10
7
|
export function visibleWidth(str) {
|
|
11
|
-
// Replace tabs with 3 spaces before measuring
|
|
12
8
|
const normalized = str.replace(/\t/g, " ");
|
|
13
9
|
return stringWidth(normalized);
|
|
14
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract ANSI escape sequences from a string at the given position.
|
|
13
|
+
*/
|
|
14
|
+
function extractAnsiCode(str, pos) {
|
|
15
|
+
if (pos >= str.length || str[pos] !== "\x1b" || str[pos + 1] !== "[") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
let j = pos + 2;
|
|
19
|
+
while (j < str.length && str[j] && !/[mGKHJ]/.test(str[j])) {
|
|
20
|
+
j++;
|
|
21
|
+
}
|
|
22
|
+
if (j < str.length) {
|
|
23
|
+
return {
|
|
24
|
+
code: str.substring(pos, j + 1),
|
|
25
|
+
length: j + 1 - pos,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Track active ANSI SGR codes to preserve styling across line breaks.
|
|
32
|
+
*/
|
|
33
|
+
class AnsiCodeTracker {
|
|
34
|
+
activeAnsiCodes = [];
|
|
35
|
+
process(ansiCode) {
|
|
36
|
+
if (!ansiCode.endsWith("m")) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Full reset clears everything
|
|
40
|
+
if (ansiCode === "\x1b[0m" || ansiCode === "\x1b[m") {
|
|
41
|
+
this.activeAnsiCodes.length = 0;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.activeAnsiCodes.push(ansiCode);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
getActiveCodes() {
|
|
48
|
+
return this.activeAnsiCodes.join("");
|
|
49
|
+
}
|
|
50
|
+
hasActiveCodes() {
|
|
51
|
+
return this.activeAnsiCodes.length > 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function updateTrackerFromText(text, tracker) {
|
|
55
|
+
let i = 0;
|
|
56
|
+
while (i < text.length) {
|
|
57
|
+
const ansiResult = extractAnsiCode(text, i);
|
|
58
|
+
if (ansiResult) {
|
|
59
|
+
tracker.process(ansiResult.code);
|
|
60
|
+
i += ansiResult.length;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Split text into words while keeping ANSI codes attached.
|
|
69
|
+
*/
|
|
70
|
+
function splitIntoWordsWithAnsi(text) {
|
|
71
|
+
const words = [];
|
|
72
|
+
let currentWord = "";
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < text.length) {
|
|
75
|
+
const char = text[i];
|
|
76
|
+
const ansiResult = extractAnsiCode(text, i);
|
|
77
|
+
if (ansiResult) {
|
|
78
|
+
currentWord += ansiResult.code;
|
|
79
|
+
i += ansiResult.length;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === " ") {
|
|
83
|
+
if (currentWord) {
|
|
84
|
+
words.push(currentWord);
|
|
85
|
+
currentWord = "";
|
|
86
|
+
}
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
currentWord += char;
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
if (currentWord) {
|
|
94
|
+
words.push(currentWord);
|
|
95
|
+
}
|
|
96
|
+
return words;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Wrap text with ANSI codes preserved.
|
|
100
|
+
*
|
|
101
|
+
* ONLY does word wrapping - NO padding, NO background colors.
|
|
102
|
+
* Returns lines where each line is <= width visible chars.
|
|
103
|
+
* Active ANSI codes are preserved across line breaks.
|
|
104
|
+
*
|
|
105
|
+
* @param text - Text to wrap (may contain ANSI codes and newlines)
|
|
106
|
+
* @param width - Maximum visible width per line
|
|
107
|
+
* @returns Array of wrapped lines (NOT padded to width)
|
|
108
|
+
*/
|
|
109
|
+
export function wrapTextWithAnsi(text, width) {
|
|
110
|
+
if (!text) {
|
|
111
|
+
return [""];
|
|
112
|
+
}
|
|
113
|
+
// Handle newlines by processing each line separately
|
|
114
|
+
const inputLines = text.split("\n");
|
|
115
|
+
const result = [];
|
|
116
|
+
for (const inputLine of inputLines) {
|
|
117
|
+
result.push(...wrapSingleLine(inputLine, width));
|
|
118
|
+
}
|
|
119
|
+
return result.length > 0 ? result : [""];
|
|
120
|
+
}
|
|
121
|
+
function wrapSingleLine(line, width) {
|
|
122
|
+
if (!line) {
|
|
123
|
+
return [""];
|
|
124
|
+
}
|
|
125
|
+
const visibleLength = visibleWidth(line);
|
|
126
|
+
if (visibleLength <= width) {
|
|
127
|
+
return [line];
|
|
128
|
+
}
|
|
129
|
+
const wrapped = [];
|
|
130
|
+
const tracker = new AnsiCodeTracker();
|
|
131
|
+
const words = splitIntoWordsWithAnsi(line);
|
|
132
|
+
let currentLine = "";
|
|
133
|
+
let currentVisibleLength = 0;
|
|
134
|
+
for (const word of words) {
|
|
135
|
+
const wordVisibleLength = visibleWidth(word);
|
|
136
|
+
// Check if adding this word would exceed width
|
|
137
|
+
const spaceNeeded = currentVisibleLength > 0 ? 1 : 0;
|
|
138
|
+
const totalNeeded = currentVisibleLength + spaceNeeded + wordVisibleLength;
|
|
139
|
+
if (totalNeeded > width && currentVisibleLength > 0) {
|
|
140
|
+
// Wrap to next line
|
|
141
|
+
wrapped.push(currentLine);
|
|
142
|
+
currentLine = tracker.getActiveCodes() + word;
|
|
143
|
+
currentVisibleLength = wordVisibleLength;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Add to current line
|
|
147
|
+
if (currentVisibleLength > 0) {
|
|
148
|
+
currentLine += " " + word;
|
|
149
|
+
currentVisibleLength += 1 + wordVisibleLength;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
currentLine += word;
|
|
153
|
+
currentVisibleLength = wordVisibleLength;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
updateTrackerFromText(word, tracker);
|
|
157
|
+
}
|
|
158
|
+
if (currentLine) {
|
|
159
|
+
wrapped.push(currentLine);
|
|
160
|
+
}
|
|
161
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Apply background color to a line, padding to full width.
|
|
165
|
+
*
|
|
166
|
+
* Handles the tricky case where content contains \x1b[0m resets that would
|
|
167
|
+
* kill the background color. We reapply the background after any reset.
|
|
168
|
+
*
|
|
169
|
+
* @param line - Line of text (may contain ANSI codes)
|
|
170
|
+
* @param width - Total width to pad to
|
|
171
|
+
* @param bgRgb - Background RGB color
|
|
172
|
+
* @returns Line with background applied and padded to width
|
|
173
|
+
*/
|
|
174
|
+
export function applyBackgroundToLine(line, width, bgRgb) {
|
|
175
|
+
const bgStart = `\x1b[48;2;${bgRgb.r};${bgRgb.g};${bgRgb.b}m`;
|
|
176
|
+
const bgEnd = "\x1b[49m";
|
|
177
|
+
// Calculate padding needed
|
|
178
|
+
const visibleLen = visibleWidth(line);
|
|
179
|
+
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
180
|
+
const padding = " ".repeat(paddingNeeded);
|
|
181
|
+
// Strategy: wrap content + padding in background, then fix any 0m resets
|
|
182
|
+
const withPadding = line + padding;
|
|
183
|
+
const withBg = bgStart + withPadding + bgEnd;
|
|
184
|
+
// Find all \x1b[0m or \x1b[49m that would kill background
|
|
185
|
+
// Replace with reset + background reapplication
|
|
186
|
+
const fixedBg = withBg.replace(/\x1b\[0m/g, `\x1b[0m${bgStart}`);
|
|
187
|
+
return fixedBg;
|
|
188
|
+
}
|
|
15
189
|
//# sourceMappingURL=utils.js.map
|