phi-code-tui 0.56.3 → 0.74.1
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/README.md +29 -11
- package/dist/autocomplete.d.ts +18 -14
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +151 -112
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +6 -1
- package/dist/components/box.js.map +1 -1
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js +6 -7
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +45 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +505 -221
- package/dist/components/editor.js.map +1 -1
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +22 -7
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +57 -74
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts +12 -2
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +36 -13
- package/dist/components/loader.js.map +1 -1
- package/dist/components/markdown.d.ts +0 -5
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +101 -114
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/select-list.d.ts +19 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +82 -71
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +18 -10
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js +1 -0
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +8 -0
- package/dist/components/text.js.map +1 -1
- package/dist/components/truncated-text.d.ts.map +1 -1
- package/dist/components/truncated-text.js +3 -0
- package/dist/components/truncated-text.js.map +1 -1
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +3 -0
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +187 -33
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +156 -95
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts +21 -12
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +270 -112
- package/dist/keys.js.map +1 -1
- package/dist/kill-ring.d.ts.map +1 -1
- package/dist/kill-ring.js +1 -3
- package/dist/kill-ring.js.map +1 -1
- package/dist/stdin-buffer.d.ts +2 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +31 -8
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +17 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +41 -5
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +4 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +56 -8
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +21 -5
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +234 -118
- package/dist/tui.js.map +1 -1
- package/dist/undo-stack.d.ts.map +1 -1
- package/dist/undo-stack.js +1 -3
- package/dist/undo-stack.js.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +281 -81
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
package/dist/utils.js
CHANGED
|
@@ -30,6 +30,98 @@ const rgiEmojiRegex = /^\p{RGI_Emoji}$/v;
|
|
|
30
30
|
// Cache for non-ASCII strings
|
|
31
31
|
const WIDTH_CACHE_SIZE = 512;
|
|
32
32
|
const widthCache = new Map();
|
|
33
|
+
function isPrintableAscii(str) {
|
|
34
|
+
for (let i = 0; i < str.length; i++) {
|
|
35
|
+
const code = str.charCodeAt(i);
|
|
36
|
+
if (code < 0x20 || code > 0x7e) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function truncateFragmentToWidth(text, maxWidth) {
|
|
43
|
+
if (maxWidth <= 0 || text.length === 0) {
|
|
44
|
+
return { text: "", width: 0 };
|
|
45
|
+
}
|
|
46
|
+
if (isPrintableAscii(text)) {
|
|
47
|
+
const clipped = text.slice(0, maxWidth);
|
|
48
|
+
return { text: clipped, width: clipped.length };
|
|
49
|
+
}
|
|
50
|
+
const hasAnsi = text.includes("\x1b");
|
|
51
|
+
const hasTabs = text.includes("\t");
|
|
52
|
+
if (!hasAnsi && !hasTabs) {
|
|
53
|
+
let result = "";
|
|
54
|
+
let width = 0;
|
|
55
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
56
|
+
const w = graphemeWidth(segment);
|
|
57
|
+
if (width + w > maxWidth) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
result += segment;
|
|
61
|
+
width += w;
|
|
62
|
+
}
|
|
63
|
+
return { text: result, width };
|
|
64
|
+
}
|
|
65
|
+
let result = "";
|
|
66
|
+
let width = 0;
|
|
67
|
+
let i = 0;
|
|
68
|
+
let pendingAnsi = "";
|
|
69
|
+
while (i < text.length) {
|
|
70
|
+
const ansi = extractAnsiCode(text, i);
|
|
71
|
+
if (ansi) {
|
|
72
|
+
pendingAnsi += ansi.code;
|
|
73
|
+
i += ansi.length;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (text[i] === "\t") {
|
|
77
|
+
if (width + 3 > maxWidth) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (pendingAnsi) {
|
|
81
|
+
result += pendingAnsi;
|
|
82
|
+
pendingAnsi = "";
|
|
83
|
+
}
|
|
84
|
+
result += "\t";
|
|
85
|
+
width += 3;
|
|
86
|
+
i++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
let end = i;
|
|
90
|
+
while (end < text.length && text[end] !== "\t") {
|
|
91
|
+
const nextAnsi = extractAnsiCode(text, end);
|
|
92
|
+
if (nextAnsi) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
end++;
|
|
96
|
+
}
|
|
97
|
+
for (const { segment } of segmenter.segment(text.slice(i, end))) {
|
|
98
|
+
const w = graphemeWidth(segment);
|
|
99
|
+
if (width + w > maxWidth) {
|
|
100
|
+
return { text: result, width };
|
|
101
|
+
}
|
|
102
|
+
if (pendingAnsi) {
|
|
103
|
+
result += pendingAnsi;
|
|
104
|
+
pendingAnsi = "";
|
|
105
|
+
}
|
|
106
|
+
result += segment;
|
|
107
|
+
width += w;
|
|
108
|
+
}
|
|
109
|
+
i = end;
|
|
110
|
+
}
|
|
111
|
+
return { text: result, width };
|
|
112
|
+
}
|
|
113
|
+
function finalizeTruncatedResult(prefix, prefixWidth, ellipsis, ellipsisWidth, maxWidth, pad) {
|
|
114
|
+
const reset = "\x1b[0m";
|
|
115
|
+
const visibleWidth = prefixWidth + ellipsisWidth;
|
|
116
|
+
let result;
|
|
117
|
+
if (ellipsis.length > 0) {
|
|
118
|
+
result = `${prefix}${reset}${ellipsis}${reset}`;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
result = `${prefix}${reset}`;
|
|
122
|
+
}
|
|
123
|
+
return pad ? result + " ".repeat(Math.max(0, maxWidth - visibleWidth)) : result;
|
|
124
|
+
}
|
|
33
125
|
/**
|
|
34
126
|
* Calculate the terminal width of a single grapheme cluster.
|
|
35
127
|
* Based on code from the string-width library, but includes a possible-emoji
|
|
@@ -57,13 +149,16 @@ function graphemeWidth(segment) {
|
|
|
57
149
|
return 2;
|
|
58
150
|
}
|
|
59
151
|
let width = eastAsianWidth(cp);
|
|
60
|
-
// Trailing halfwidth/fullwidth forms
|
|
152
|
+
// Trailing halfwidth/fullwidth forms and AM vowels that segment with a base.
|
|
61
153
|
if (segment.length > 1) {
|
|
62
154
|
for (const char of segment.slice(1)) {
|
|
63
155
|
const c = char.codePointAt(0);
|
|
64
156
|
if (c >= 0xff00 && c <= 0xffef) {
|
|
65
157
|
width += eastAsianWidth(c);
|
|
66
158
|
}
|
|
159
|
+
else if (c === 0x0e33 || c === 0x0eb3) {
|
|
160
|
+
width += 1;
|
|
161
|
+
}
|
|
67
162
|
}
|
|
68
163
|
}
|
|
69
164
|
return width;
|
|
@@ -76,15 +171,7 @@ export function visibleWidth(str) {
|
|
|
76
171
|
return 0;
|
|
77
172
|
}
|
|
78
173
|
// Fast path: pure ASCII printable
|
|
79
|
-
|
|
80
|
-
for (let i = 0; i < str.length; i++) {
|
|
81
|
-
const code = str.charCodeAt(i);
|
|
82
|
-
if (code < 0x20 || code > 0x7e) {
|
|
83
|
-
isPureAscii = false;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (isPureAscii) {
|
|
174
|
+
if (isPrintableAscii(str)) {
|
|
88
175
|
return str.length;
|
|
89
176
|
}
|
|
90
177
|
// Check cache
|
|
@@ -129,6 +216,19 @@ export function visibleWidth(str) {
|
|
|
129
216
|
widthCache.set(str, width);
|
|
130
217
|
return width;
|
|
131
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Normalize text for terminal output without changing logical editor content.
|
|
221
|
+
* Some terminals render precomposed Thai/Lao AM vowels inconsistently during
|
|
222
|
+
* differential repaint. Their compatibility decompositions have the same cell
|
|
223
|
+
* width but avoid stale-cell artifacts in terminal renderers.
|
|
224
|
+
*/
|
|
225
|
+
const THAI_LAO_AM_REGEX = /[\u0e33\u0eb3]/;
|
|
226
|
+
const THAI_LAO_AM_GLOBAL_REGEX = /[\u0e33\u0eb3]/g;
|
|
227
|
+
export function normalizeTerminalOutput(str) {
|
|
228
|
+
if (!THAI_LAO_AM_REGEX.test(str))
|
|
229
|
+
return str;
|
|
230
|
+
return str.replace(THAI_LAO_AM_GLOBAL_REGEX, (char) => (char === "\u0e33" ? "\u0e4d\u0e32" : "\u0ecd\u0eb2"));
|
|
231
|
+
}
|
|
132
232
|
/**
|
|
133
233
|
* Extract ANSI escape sequences from a string at the given position.
|
|
134
234
|
*/
|
|
@@ -173,24 +273,55 @@ export function extractAnsiCode(str, pos) {
|
|
|
173
273
|
}
|
|
174
274
|
return null;
|
|
175
275
|
}
|
|
276
|
+
function parseOsc8Hyperlink(ansiCode) {
|
|
277
|
+
if (!ansiCode.startsWith("\x1b]8;")) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const terminator = ansiCode.endsWith("\x07") ? "\x07" : "\x1b\\";
|
|
281
|
+
const body = ansiCode.slice(4, terminator === "\x07" ? -1 : -2);
|
|
282
|
+
const separatorIndex = body.indexOf(";");
|
|
283
|
+
if (separatorIndex === -1) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const params = body.slice(0, separatorIndex);
|
|
287
|
+
const url = body.slice(separatorIndex + 1);
|
|
288
|
+
if (!url) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return { params, url, terminator };
|
|
292
|
+
}
|
|
293
|
+
function formatOsc8Hyperlink(hyperlink) {
|
|
294
|
+
return `\x1b]8;${hyperlink.params};${hyperlink.url}${hyperlink.terminator}`;
|
|
295
|
+
}
|
|
296
|
+
function formatOsc8Close(terminator) {
|
|
297
|
+
return `\x1b]8;;${terminator}`;
|
|
298
|
+
}
|
|
176
299
|
/**
|
|
177
300
|
* Track active ANSI SGR codes to preserve styling across line breaks.
|
|
178
301
|
*/
|
|
179
302
|
class AnsiCodeTracker {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
303
|
+
// Track individual attributes separately so we can reset them specifically
|
|
304
|
+
bold = false;
|
|
305
|
+
dim = false;
|
|
306
|
+
italic = false;
|
|
307
|
+
underline = false;
|
|
308
|
+
blink = false;
|
|
309
|
+
inverse = false;
|
|
310
|
+
hidden = false;
|
|
311
|
+
strikethrough = false;
|
|
312
|
+
fgColor = null; // Stores the full code like "31" or "38;5;240"
|
|
313
|
+
bgColor = null; // Stores the full code like "41" or "48;5;240"
|
|
314
|
+
activeHyperlink = null;
|
|
193
315
|
process(ansiCode) {
|
|
316
|
+
// OSC 8 hyperlink: \x1b]8;;<url>\x1b\\ (open) or \x1b]8;;\x1b\\ (close).
|
|
317
|
+
// Preserve the original terminator because some terminals only make BEL-terminated
|
|
318
|
+
// links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST
|
|
319
|
+
// made only the first physical line clickable in those terminals.
|
|
320
|
+
const hyperlink = parseOsc8Hyperlink(ansiCode);
|
|
321
|
+
if (hyperlink !== undefined) {
|
|
322
|
+
this.activeHyperlink = hyperlink;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
194
325
|
if (!ansiCode.endsWith("m")) {
|
|
195
326
|
return;
|
|
196
327
|
}
|
|
@@ -323,10 +454,12 @@ class AnsiCodeTracker {
|
|
|
323
454
|
this.strikethrough = false;
|
|
324
455
|
this.fgColor = null;
|
|
325
456
|
this.bgColor = null;
|
|
457
|
+
// SGR reset does not affect OSC 8 hyperlink state
|
|
326
458
|
}
|
|
327
459
|
/** Clear all state for reuse. */
|
|
328
460
|
clear() {
|
|
329
461
|
this.reset();
|
|
462
|
+
this.activeHyperlink = null;
|
|
330
463
|
}
|
|
331
464
|
getActiveCodes() {
|
|
332
465
|
const codes = [];
|
|
@@ -350,9 +483,11 @@ class AnsiCodeTracker {
|
|
|
350
483
|
codes.push(this.fgColor);
|
|
351
484
|
if (this.bgColor)
|
|
352
485
|
codes.push(this.bgColor);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
486
|
+
let result = codes.length > 0 ? `\x1b[${codes.join(";")}m` : "";
|
|
487
|
+
if (this.activeHyperlink) {
|
|
488
|
+
result += formatOsc8Hyperlink(this.activeHyperlink);
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
356
491
|
}
|
|
357
492
|
hasActiveCodes() {
|
|
358
493
|
return (this.bold ||
|
|
@@ -364,20 +499,24 @@ class AnsiCodeTracker {
|
|
|
364
499
|
this.hidden ||
|
|
365
500
|
this.strikethrough ||
|
|
366
501
|
this.fgColor !== null ||
|
|
367
|
-
this.bgColor !== null
|
|
502
|
+
this.bgColor !== null ||
|
|
503
|
+
this.activeHyperlink !== null);
|
|
368
504
|
}
|
|
369
505
|
/**
|
|
370
|
-
* Get reset codes for attributes that need to be turned off at line end
|
|
371
|
-
*
|
|
372
|
-
*
|
|
506
|
+
* Get reset codes for attributes that need to be turned off at line end.
|
|
507
|
+
* Underline must be closed to prevent bleeding into padding.
|
|
508
|
+
* Active OSC 8 hyperlinks must be closed and re-opened on the next line.
|
|
509
|
+
* Returns empty string if no attributes need closing.
|
|
373
510
|
*/
|
|
374
511
|
getLineEndReset() {
|
|
375
|
-
|
|
376
|
-
// Other attributes like colors don't visually bleed to padding
|
|
512
|
+
let result = "";
|
|
377
513
|
if (this.underline) {
|
|
378
|
-
|
|
514
|
+
result += "\x1b[24m"; // Underline off only
|
|
379
515
|
}
|
|
380
|
-
|
|
516
|
+
if (this.activeHyperlink) {
|
|
517
|
+
result += formatOsc8Close(this.activeHyperlink.terminator); // Re-opened at line start via getActiveCodes()
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
381
520
|
}
|
|
382
521
|
}
|
|
383
522
|
function updateTrackerFromText(text, tracker) {
|
|
@@ -637,67 +776,128 @@ export function applyBackgroundToLine(line, width, bgFn) {
|
|
|
637
776
|
* @returns Truncated text, optionally padded to exactly maxWidth
|
|
638
777
|
*/
|
|
639
778
|
export function truncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
779
|
+
if (maxWidth <= 0) {
|
|
780
|
+
return "";
|
|
781
|
+
}
|
|
782
|
+
if (text.length === 0) {
|
|
783
|
+
return pad ? " ".repeat(maxWidth) : "";
|
|
643
784
|
}
|
|
644
785
|
const ellipsisWidth = visibleWidth(ellipsis);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
786
|
+
if (ellipsisWidth >= maxWidth) {
|
|
787
|
+
const textWidth = visibleWidth(text);
|
|
788
|
+
if (textWidth <= maxWidth) {
|
|
789
|
+
return pad ? text + " ".repeat(maxWidth - textWidth) : text;
|
|
790
|
+
}
|
|
791
|
+
const clippedEllipsis = truncateFragmentToWidth(ellipsis, maxWidth);
|
|
792
|
+
if (clippedEllipsis.width === 0) {
|
|
793
|
+
return pad ? " ".repeat(maxWidth) : "";
|
|
794
|
+
}
|
|
795
|
+
return finalizeTruncatedResult("", 0, clippedEllipsis.text, clippedEllipsis.width, maxWidth, pad);
|
|
648
796
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
while (i < text.length) {
|
|
653
|
-
const ansiResult = extractAnsiCode(text, i);
|
|
654
|
-
if (ansiResult) {
|
|
655
|
-
segments.push({ type: "ansi", value: ansiResult.code });
|
|
656
|
-
i += ansiResult.length;
|
|
797
|
+
if (isPrintableAscii(text)) {
|
|
798
|
+
if (text.length <= maxWidth) {
|
|
799
|
+
return pad ? text + " ".repeat(maxWidth - text.length) : text;
|
|
657
800
|
}
|
|
658
|
-
|
|
659
|
-
|
|
801
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
802
|
+
return finalizeTruncatedResult(text.slice(0, targetWidth), targetWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
803
|
+
}
|
|
804
|
+
const targetWidth = maxWidth - ellipsisWidth;
|
|
805
|
+
let result = "";
|
|
806
|
+
let pendingAnsi = "";
|
|
807
|
+
let visibleSoFar = 0;
|
|
808
|
+
let keptWidth = 0;
|
|
809
|
+
let keepContiguousPrefix = true;
|
|
810
|
+
let overflowed = false;
|
|
811
|
+
let exhaustedInput = false;
|
|
812
|
+
const hasAnsi = text.includes("\x1b");
|
|
813
|
+
const hasTabs = text.includes("\t");
|
|
814
|
+
if (!hasAnsi && !hasTabs) {
|
|
815
|
+
for (const { segment } of segmenter.segment(text)) {
|
|
816
|
+
const width = graphemeWidth(segment);
|
|
817
|
+
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
818
|
+
result += segment;
|
|
819
|
+
keptWidth += width;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
keepContiguousPrefix = false;
|
|
823
|
+
}
|
|
824
|
+
visibleSoFar += width;
|
|
825
|
+
if (visibleSoFar > maxWidth) {
|
|
826
|
+
overflowed = true;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
exhaustedInput = !overflowed;
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
let i = 0;
|
|
834
|
+
while (i < text.length) {
|
|
835
|
+
const ansi = extractAnsiCode(text, i);
|
|
836
|
+
if (ansi) {
|
|
837
|
+
pendingAnsi += ansi.code;
|
|
838
|
+
i += ansi.length;
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (text[i] === "\t") {
|
|
842
|
+
if (keepContiguousPrefix && keptWidth + 3 <= targetWidth) {
|
|
843
|
+
if (pendingAnsi) {
|
|
844
|
+
result += pendingAnsi;
|
|
845
|
+
pendingAnsi = "";
|
|
846
|
+
}
|
|
847
|
+
result += "\t";
|
|
848
|
+
keptWidth += 3;
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
keepContiguousPrefix = false;
|
|
852
|
+
pendingAnsi = "";
|
|
853
|
+
}
|
|
854
|
+
visibleSoFar += 3;
|
|
855
|
+
if (visibleSoFar > maxWidth) {
|
|
856
|
+
overflowed = true;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
i++;
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
660
862
|
let end = i;
|
|
661
|
-
while (end < text.length) {
|
|
863
|
+
while (end < text.length && text[end] !== "\t") {
|
|
662
864
|
const nextAnsi = extractAnsiCode(text, end);
|
|
663
|
-
if (nextAnsi)
|
|
865
|
+
if (nextAnsi) {
|
|
664
866
|
break;
|
|
867
|
+
}
|
|
665
868
|
end++;
|
|
666
869
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
870
|
+
for (const { segment } of segmenter.segment(text.slice(i, end))) {
|
|
871
|
+
const width = graphemeWidth(segment);
|
|
872
|
+
if (keepContiguousPrefix && keptWidth + width <= targetWidth) {
|
|
873
|
+
if (pendingAnsi) {
|
|
874
|
+
result += pendingAnsi;
|
|
875
|
+
pendingAnsi = "";
|
|
876
|
+
}
|
|
877
|
+
result += segment;
|
|
878
|
+
keptWidth += width;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
keepContiguousPrefix = false;
|
|
882
|
+
pendingAnsi = "";
|
|
883
|
+
}
|
|
884
|
+
visibleSoFar += width;
|
|
885
|
+
if (visibleSoFar > maxWidth) {
|
|
886
|
+
overflowed = true;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (overflowed) {
|
|
891
|
+
break;
|
|
671
892
|
}
|
|
672
893
|
i = end;
|
|
673
894
|
}
|
|
895
|
+
exhaustedInput = i >= text.length;
|
|
674
896
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
let currentWidth = 0;
|
|
678
|
-
for (const seg of segments) {
|
|
679
|
-
if (seg.type === "ansi") {
|
|
680
|
-
result += seg.value;
|
|
681
|
-
continue;
|
|
682
|
-
}
|
|
683
|
-
const grapheme = seg.value;
|
|
684
|
-
// Skip empty graphemes to avoid issues with string-width calculation
|
|
685
|
-
if (!grapheme)
|
|
686
|
-
continue;
|
|
687
|
-
const graphemeWidth = visibleWidth(grapheme);
|
|
688
|
-
if (currentWidth + graphemeWidth > targetWidth) {
|
|
689
|
-
break;
|
|
690
|
-
}
|
|
691
|
-
result += grapheme;
|
|
692
|
-
currentWidth += graphemeWidth;
|
|
693
|
-
}
|
|
694
|
-
// Add reset code before ellipsis to prevent styling leaking into it
|
|
695
|
-
const truncated = `${result}\x1b[0m${ellipsis}`;
|
|
696
|
-
if (pad) {
|
|
697
|
-
const truncatedWidth = visibleWidth(truncated);
|
|
698
|
-
return truncated + " ".repeat(Math.max(0, maxWidth - truncatedWidth));
|
|
897
|
+
if (!overflowed && exhaustedInput) {
|
|
898
|
+
return pad ? text + " ".repeat(Math.max(0, maxWidth - visibleSoFar)) : text;
|
|
699
899
|
}
|
|
700
|
-
return
|
|
900
|
+
return finalizeTruncatedResult(result, keptWidth, ellipsis, ellipsisWidth, maxWidth, pad);
|
|
701
901
|
}
|
|
702
902
|
/**
|
|
703
903
|
* Extract a range of visible columns from a line. Handles ANSI codes and wide chars.
|