@oh-my-pi/pi-tui 8.12.5 → 8.12.7
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/package.json +2 -1
- package/src/components/editor.ts +22 -14
- package/src/keys.ts +13 -3
- package/src/utils.ts +27 -392
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-tui",
|
|
3
|
-
"version": "8.12.
|
|
3
|
+
"version": "8.12.7",
|
|
4
4
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"bun": ">=1.3.7"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@oh-my-pi/pi-natives": "8.12.7",
|
|
50
51
|
"@types/mime-types": "^3.0.1",
|
|
51
52
|
"chalk": "^5.6.2",
|
|
52
53
|
"marked": "^17.0.1",
|
package/src/components/editor.ts
CHANGED
|
@@ -743,21 +743,29 @@ export class Editor implements Component, Focusable {
|
|
|
743
743
|
(matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") &&
|
|
744
744
|
this.autocompletePrefix.startsWith("/")
|
|
745
745
|
) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
)
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
746
|
+
// Check for stale autocomplete state due to debounce
|
|
747
|
+
const currentLine = this.state.lines[this.state.cursorLine] ?? "";
|
|
748
|
+
const currentTextBeforeCursor = currentLine.slice(0, this.state.cursorCol);
|
|
749
|
+
if (currentTextBeforeCursor !== this.autocompletePrefix) {
|
|
750
|
+
// Autocomplete is stale - cancel and fall through to normal submission
|
|
751
|
+
this.cancelAutocomplete();
|
|
752
|
+
} else {
|
|
753
|
+
const selected = this.autocompleteList.getSelectedItem();
|
|
754
|
+
if (selected && this.autocompleteProvider) {
|
|
755
|
+
const result = this.autocompleteProvider.applyCompletion(
|
|
756
|
+
this.state.lines,
|
|
757
|
+
this.state.cursorLine,
|
|
758
|
+
this.state.cursorCol,
|
|
759
|
+
selected,
|
|
760
|
+
this.autocompletePrefix,
|
|
761
|
+
);
|
|
762
|
+
|
|
763
|
+
this.state.lines = result.lines;
|
|
764
|
+
this.state.cursorLine = result.cursorLine;
|
|
765
|
+
this.state.cursorCol = result.cursorCol;
|
|
766
|
+
}
|
|
767
|
+
this.cancelAutocomplete();
|
|
759
768
|
}
|
|
760
|
-
this.cancelAutocomplete();
|
|
761
769
|
// Don't return - fall through to submission logic
|
|
762
770
|
}
|
|
763
771
|
// If Enter was pressed on a file path, apply completion
|
package/src/keys.ts
CHANGED
|
@@ -664,16 +664,26 @@ function rawCtrlChar(letter: string): string {
|
|
|
664
664
|
return String.fromCharCode(code);
|
|
665
665
|
}
|
|
666
666
|
|
|
667
|
-
|
|
668
|
-
|
|
667
|
+
type ParsedKeyId = { key: string; ctrl: boolean; shift: boolean; alt: boolean };
|
|
668
|
+
|
|
669
|
+
const PARSED_KEY_ID_CACHE = new Map<string, ParsedKeyId>();
|
|
670
|
+
|
|
671
|
+
function parseKeyId(keyId: string): ParsedKeyId | null {
|
|
672
|
+
const normalizedKeyId = keyId.toLowerCase();
|
|
673
|
+
const cached = PARSED_KEY_ID_CACHE.get(normalizedKeyId);
|
|
674
|
+
if (cached) return cached;
|
|
675
|
+
|
|
676
|
+
const parts = normalizedKeyId.split("+");
|
|
669
677
|
const key = parts[parts.length - 1];
|
|
670
678
|
if (!key) return null;
|
|
671
|
-
|
|
679
|
+
const parsed = {
|
|
672
680
|
key,
|
|
673
681
|
ctrl: parts.includes("ctrl"),
|
|
674
682
|
shift: parts.includes("shift"),
|
|
675
683
|
alt: parts.includes("alt"),
|
|
676
684
|
};
|
|
685
|
+
PARSED_KEY_ID_CACHE.set(normalizedKeyId, parsed);
|
|
686
|
+
return parsed;
|
|
677
687
|
}
|
|
678
688
|
|
|
679
689
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractSegments as nativeExtractSegments,
|
|
3
|
+
sliceWithWidth as nativeSliceWithWidth,
|
|
4
|
+
truncateToWidth as nativeTruncateToWidth,
|
|
5
|
+
visibleWidth as nativeVisibleWidth,
|
|
6
|
+
} from "@oh-my-pi/pi-natives";
|
|
7
|
+
|
|
1
8
|
// Grapheme segmenter (shared instance)
|
|
2
9
|
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
10
|
|
|
@@ -11,6 +18,7 @@ export function getSegmenter(): Intl.Segmenter {
|
|
|
11
18
|
// Cache for non-ASCII strings
|
|
12
19
|
const WIDTH_CACHE_SIZE = 512;
|
|
13
20
|
const widthCache = new Map<string, number>();
|
|
21
|
+
const NATIVE_WIDTH_THRESHOLD = 256;
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
24
|
* Calculate the visible width of a string in terminal columns.
|
|
@@ -39,20 +47,24 @@ export function visibleWidth(str: string): number {
|
|
|
39
47
|
return cached;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
clean =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
let width: number;
|
|
51
|
+
if (str.length <= NATIVE_WIDTH_THRESHOLD) {
|
|
52
|
+
// Normalize: tabs to 3 spaces, strip ANSI escape codes
|
|
53
|
+
let clean = str;
|
|
54
|
+
if (str.includes("\t")) {
|
|
55
|
+
clean = clean.replace(/\t/g, " ");
|
|
56
|
+
}
|
|
57
|
+
if (clean.includes("\x1b")) {
|
|
58
|
+
// Strip SGR codes (\x1b[...m) and cursor codes (\x1b[...G/K/H/J)
|
|
59
|
+
clean = clean.replace(/\x1b\[[0-9;]*[mGKHJ]/g, "");
|
|
60
|
+
// Strip OSC 8 hyperlinks: \x1b]8;;URL\x07 and \x1b]8;;\x07
|
|
61
|
+
clean = clean.replace(/\x1b\]8;;[^\x07]*\x07/g, "");
|
|
62
|
+
}
|
|
63
|
+
width = Bun.stringWidth(clean);
|
|
64
|
+
} else {
|
|
65
|
+
width = nativeVisibleWidth(str);
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
const width = Bun.stringWidth(clean);
|
|
55
|
-
|
|
56
68
|
// Cache result
|
|
57
69
|
if (widthCache.size >= WIDTH_CACHE_SIZE) {
|
|
58
70
|
const firstKey = widthCache.keys().next().value;
|
|
@@ -98,211 +110,6 @@ export function extractAnsiCode(str: string, pos: number): { code: string; lengt
|
|
|
98
110
|
return null;
|
|
99
111
|
}
|
|
100
112
|
|
|
101
|
-
/**
|
|
102
|
-
* Track active ANSI SGR codes to preserve styling across line breaks.
|
|
103
|
-
*/
|
|
104
|
-
class AnsiCodeTracker {
|
|
105
|
-
// Track individual attributes separately so we can reset them specifically
|
|
106
|
-
private bold = false;
|
|
107
|
-
private dim = false;
|
|
108
|
-
private italic = false;
|
|
109
|
-
private underline = false;
|
|
110
|
-
private blink = false;
|
|
111
|
-
private inverse = false;
|
|
112
|
-
private hidden = false;
|
|
113
|
-
private strikethrough = false;
|
|
114
|
-
private fgColor: string | null = null; // Stores the full code like "31" or "38;5;240"
|
|
115
|
-
private bgColor: string | null = null; // Stores the full code like "41" or "48;5;240"
|
|
116
|
-
|
|
117
|
-
process(ansiCode: string): void {
|
|
118
|
-
if (!ansiCode.endsWith("m")) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Extract the parameters between \x1b[ and m
|
|
123
|
-
const match = ansiCode.match(/\x1b\[([\d;]*)m/);
|
|
124
|
-
if (!match) return;
|
|
125
|
-
|
|
126
|
-
const params = match[1];
|
|
127
|
-
if (params === "" || params === "0") {
|
|
128
|
-
// Full reset
|
|
129
|
-
this.reset();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Parse parameters (can be semicolon-separated)
|
|
134
|
-
const parts = params.split(";");
|
|
135
|
-
let i = 0;
|
|
136
|
-
while (i < parts.length) {
|
|
137
|
-
const code = Number.parseInt(parts[i], 10);
|
|
138
|
-
|
|
139
|
-
// Handle 256-color and RGB codes which consume multiple parameters
|
|
140
|
-
if (code === 38 || code === 48) {
|
|
141
|
-
// 38;5;N (256 color fg) or 38;2;R;G;B (RGB fg)
|
|
142
|
-
// 48;5;N (256 color bg) or 48;2;R;G;B (RGB bg)
|
|
143
|
-
if (parts[i + 1] === "5" && parts[i + 2] !== undefined) {
|
|
144
|
-
// 256 color: 38;5;N or 48;5;N
|
|
145
|
-
const colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]}`;
|
|
146
|
-
if (code === 38) {
|
|
147
|
-
this.fgColor = colorCode;
|
|
148
|
-
} else {
|
|
149
|
-
this.bgColor = colorCode;
|
|
150
|
-
}
|
|
151
|
-
i += 3;
|
|
152
|
-
continue;
|
|
153
|
-
} else if (parts[i + 1] === "2" && parts[i + 4] !== undefined) {
|
|
154
|
-
// RGB color: 38;2;R;G;B or 48;2;R;G;B
|
|
155
|
-
const colorCode = `${parts[i]};${parts[i + 1]};${parts[i + 2]};${parts[i + 3]};${parts[i + 4]}`;
|
|
156
|
-
if (code === 38) {
|
|
157
|
-
this.fgColor = colorCode;
|
|
158
|
-
} else {
|
|
159
|
-
this.bgColor = colorCode;
|
|
160
|
-
}
|
|
161
|
-
i += 5;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Standard SGR codes
|
|
167
|
-
switch (code) {
|
|
168
|
-
case 0:
|
|
169
|
-
this.reset();
|
|
170
|
-
break;
|
|
171
|
-
case 1:
|
|
172
|
-
this.bold = true;
|
|
173
|
-
break;
|
|
174
|
-
case 2:
|
|
175
|
-
this.dim = true;
|
|
176
|
-
break;
|
|
177
|
-
case 3:
|
|
178
|
-
this.italic = true;
|
|
179
|
-
break;
|
|
180
|
-
case 4:
|
|
181
|
-
this.underline = true;
|
|
182
|
-
break;
|
|
183
|
-
case 5:
|
|
184
|
-
this.blink = true;
|
|
185
|
-
break;
|
|
186
|
-
case 7:
|
|
187
|
-
this.inverse = true;
|
|
188
|
-
break;
|
|
189
|
-
case 8:
|
|
190
|
-
this.hidden = true;
|
|
191
|
-
break;
|
|
192
|
-
case 9:
|
|
193
|
-
this.strikethrough = true;
|
|
194
|
-
break;
|
|
195
|
-
case 21:
|
|
196
|
-
this.bold = false;
|
|
197
|
-
break; // Some terminals
|
|
198
|
-
case 22:
|
|
199
|
-
this.bold = false;
|
|
200
|
-
this.dim = false;
|
|
201
|
-
break;
|
|
202
|
-
case 23:
|
|
203
|
-
this.italic = false;
|
|
204
|
-
break;
|
|
205
|
-
case 24:
|
|
206
|
-
this.underline = false;
|
|
207
|
-
break;
|
|
208
|
-
case 25:
|
|
209
|
-
this.blink = false;
|
|
210
|
-
break;
|
|
211
|
-
case 27:
|
|
212
|
-
this.inverse = false;
|
|
213
|
-
break;
|
|
214
|
-
case 28:
|
|
215
|
-
this.hidden = false;
|
|
216
|
-
break;
|
|
217
|
-
case 29:
|
|
218
|
-
this.strikethrough = false;
|
|
219
|
-
break;
|
|
220
|
-
case 39:
|
|
221
|
-
this.fgColor = null;
|
|
222
|
-
break; // Default fg
|
|
223
|
-
case 49:
|
|
224
|
-
this.bgColor = null;
|
|
225
|
-
break; // Default bg
|
|
226
|
-
default:
|
|
227
|
-
// Standard foreground colors 30-37, 90-97
|
|
228
|
-
if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
|
229
|
-
this.fgColor = String(code);
|
|
230
|
-
}
|
|
231
|
-
// Standard background colors 40-47, 100-107
|
|
232
|
-
else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
|
233
|
-
this.bgColor = String(code);
|
|
234
|
-
}
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
i++;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private reset(): void {
|
|
242
|
-
this.bold = false;
|
|
243
|
-
this.dim = false;
|
|
244
|
-
this.italic = false;
|
|
245
|
-
this.underline = false;
|
|
246
|
-
this.blink = false;
|
|
247
|
-
this.inverse = false;
|
|
248
|
-
this.hidden = false;
|
|
249
|
-
this.strikethrough = false;
|
|
250
|
-
this.fgColor = null;
|
|
251
|
-
this.bgColor = null;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Clear all state for reuse. */
|
|
255
|
-
clear(): void {
|
|
256
|
-
this.reset();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
getActiveCodes(): string {
|
|
260
|
-
const codes: string[] = [];
|
|
261
|
-
if (this.bold) codes.push("1");
|
|
262
|
-
if (this.dim) codes.push("2");
|
|
263
|
-
if (this.italic) codes.push("3");
|
|
264
|
-
if (this.underline) codes.push("4");
|
|
265
|
-
if (this.blink) codes.push("5");
|
|
266
|
-
if (this.inverse) codes.push("7");
|
|
267
|
-
if (this.hidden) codes.push("8");
|
|
268
|
-
if (this.strikethrough) codes.push("9");
|
|
269
|
-
if (this.fgColor) codes.push(this.fgColor);
|
|
270
|
-
if (this.bgColor) codes.push(this.bgColor);
|
|
271
|
-
|
|
272
|
-
if (codes.length === 0) return "";
|
|
273
|
-
return `\x1b[${codes.join(";")}m`;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
hasActiveCodes(): boolean {
|
|
277
|
-
return (
|
|
278
|
-
this.bold ||
|
|
279
|
-
this.dim ||
|
|
280
|
-
this.italic ||
|
|
281
|
-
this.underline ||
|
|
282
|
-
this.blink ||
|
|
283
|
-
this.inverse ||
|
|
284
|
-
this.hidden ||
|
|
285
|
-
this.strikethrough ||
|
|
286
|
-
this.fgColor !== null ||
|
|
287
|
-
this.bgColor !== null
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Get reset codes for attributes that need to be turned off at line end,
|
|
293
|
-
* specifically underline which bleeds into padding.
|
|
294
|
-
* Returns empty string if no problematic attributes are active.
|
|
295
|
-
*/
|
|
296
|
-
getLineEndReset(): string {
|
|
297
|
-
// Only underline causes visual bleeding into padding
|
|
298
|
-
// Other attributes like colors don't visually bleed to padding
|
|
299
|
-
if (this.underline) {
|
|
300
|
-
return "\x1b[24m"; // Underline off only
|
|
301
|
-
}
|
|
302
|
-
return "";
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
113
|
const WRAP_OPTIONS = { wordWrap: true, hard: true, trim: false } as const;
|
|
307
114
|
|
|
308
115
|
/**
|
|
@@ -367,76 +174,7 @@ export function applyBackgroundToLine(line: string, width: number, bgFn: (text:
|
|
|
367
174
|
* @returns Truncated text, optionally padded to exactly maxWidth
|
|
368
175
|
*/
|
|
369
176
|
export function truncateToWidth(text: string, maxWidth: number, ellipsis: string = "…", pad: boolean = false): string {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (textVisibleWidth <= maxWidth) {
|
|
373
|
-
return pad ? text + " ".repeat(maxWidth - textVisibleWidth) : text;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const ellipsisWidth = visibleWidth(ellipsis);
|
|
377
|
-
const targetWidth = maxWidth - ellipsisWidth;
|
|
378
|
-
|
|
379
|
-
if (targetWidth <= 0) {
|
|
380
|
-
return ellipsis.substring(0, maxWidth);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Separate ANSI codes from visible content using grapheme segmentation
|
|
384
|
-
let i = 0;
|
|
385
|
-
const segments: Array<{ type: "ansi" | "grapheme"; value: string }> = [];
|
|
386
|
-
|
|
387
|
-
while (i < text.length) {
|
|
388
|
-
const ansiResult = extractAnsiCode(text, i);
|
|
389
|
-
if (ansiResult) {
|
|
390
|
-
segments.push({ type: "ansi", value: ansiResult.code });
|
|
391
|
-
i += ansiResult.length;
|
|
392
|
-
} else {
|
|
393
|
-
// Find the next ANSI code or end of string
|
|
394
|
-
let end = i;
|
|
395
|
-
while (end < text.length) {
|
|
396
|
-
const nextAnsi = extractAnsiCode(text, end);
|
|
397
|
-
if (nextAnsi) break;
|
|
398
|
-
end++;
|
|
399
|
-
}
|
|
400
|
-
// Segment this non-ANSI portion into graphemes
|
|
401
|
-
const textPortion = text.slice(i, end);
|
|
402
|
-
for (const seg of segmenter.segment(textPortion)) {
|
|
403
|
-
segments.push({ type: "grapheme", value: seg.segment });
|
|
404
|
-
}
|
|
405
|
-
i = end;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Build truncated string from segments
|
|
410
|
-
let result = "";
|
|
411
|
-
let currentWidth = 0;
|
|
412
|
-
|
|
413
|
-
for (const seg of segments) {
|
|
414
|
-
if (seg.type === "ansi") {
|
|
415
|
-
result += seg.value;
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const grapheme = seg.value;
|
|
420
|
-
// Skip empty graphemes to avoid issues with string-width calculation
|
|
421
|
-
if (!grapheme) continue;
|
|
422
|
-
|
|
423
|
-
const graphemeWidth = visibleWidth(grapheme);
|
|
424
|
-
|
|
425
|
-
if (currentWidth + graphemeWidth > targetWidth) {
|
|
426
|
-
break;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
result += grapheme;
|
|
430
|
-
currentWidth += graphemeWidth;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Add reset code before ellipsis to prevent styling leaking into it
|
|
434
|
-
const truncated = `${result}\x1b[0m${ellipsis}`;
|
|
435
|
-
if (pad) {
|
|
436
|
-
const truncatedWidth = visibleWidth(truncated);
|
|
437
|
-
return truncated + " ".repeat(Math.max(0, maxWidth - truncatedWidth));
|
|
438
|
-
}
|
|
439
|
-
return truncated;
|
|
177
|
+
return nativeTruncateToWidth(text, maxWidth, ellipsis, pad);
|
|
440
178
|
}
|
|
441
179
|
|
|
442
180
|
/**
|
|
@@ -455,49 +193,9 @@ export function sliceWithWidth(
|
|
|
455
193
|
strict = false,
|
|
456
194
|
): { text: string; width: number } {
|
|
457
195
|
if (length <= 0) return { text: "", width: 0 };
|
|
458
|
-
|
|
459
|
-
let result = "",
|
|
460
|
-
resultWidth = 0,
|
|
461
|
-
currentCol = 0,
|
|
462
|
-
i = 0,
|
|
463
|
-
pendingAnsi = "";
|
|
464
|
-
|
|
465
|
-
while (i < line.length) {
|
|
466
|
-
const ansi = extractAnsiCode(line, i);
|
|
467
|
-
if (ansi) {
|
|
468
|
-
if (currentCol >= startCol && currentCol < endCol) result += ansi.code;
|
|
469
|
-
else if (currentCol < startCol) pendingAnsi += ansi.code;
|
|
470
|
-
i += ansi.length;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
let textEnd = i;
|
|
475
|
-
while (textEnd < line.length && !extractAnsiCode(line, textEnd)) textEnd++;
|
|
476
|
-
|
|
477
|
-
for (const { segment } of segmenter.segment(line.slice(i, textEnd))) {
|
|
478
|
-
const w = visibleWidth(segment);
|
|
479
|
-
const inRange = currentCol >= startCol && currentCol < endCol;
|
|
480
|
-
const fits = !strict || currentCol + w <= endCol;
|
|
481
|
-
if (inRange && fits) {
|
|
482
|
-
if (pendingAnsi) {
|
|
483
|
-
result += pendingAnsi;
|
|
484
|
-
pendingAnsi = "";
|
|
485
|
-
}
|
|
486
|
-
result += segment;
|
|
487
|
-
resultWidth += w;
|
|
488
|
-
}
|
|
489
|
-
currentCol += w;
|
|
490
|
-
if (currentCol >= endCol) break;
|
|
491
|
-
}
|
|
492
|
-
i = textEnd;
|
|
493
|
-
if (currentCol >= endCol) break;
|
|
494
|
-
}
|
|
495
|
-
return { text: result, width: resultWidth };
|
|
196
|
+
return nativeSliceWithWidth(line, startCol, length, strict);
|
|
496
197
|
}
|
|
497
198
|
|
|
498
|
-
// Pooled tracker instance for extractSegments (avoids allocation per call)
|
|
499
|
-
const pooledStyleTracker = new AnsiCodeTracker();
|
|
500
|
-
|
|
501
199
|
/**
|
|
502
200
|
* Extract "before" and "after" segments from a line in a single pass.
|
|
503
201
|
* Used for overlay compositing where we need content before and after the overlay region.
|
|
@@ -510,68 +208,5 @@ export function extractSegments(
|
|
|
510
208
|
afterLen: number,
|
|
511
209
|
strictAfter = false,
|
|
512
210
|
): { before: string; beforeWidth: number; after: string; afterWidth: number } {
|
|
513
|
-
|
|
514
|
-
beforeWidth = 0,
|
|
515
|
-
after = "",
|
|
516
|
-
afterWidth = 0;
|
|
517
|
-
let currentCol = 0,
|
|
518
|
-
i = 0;
|
|
519
|
-
let pendingAnsiBefore = "";
|
|
520
|
-
let afterStarted = false;
|
|
521
|
-
const afterEnd = afterStart + afterLen;
|
|
522
|
-
|
|
523
|
-
// Track styling state so "after" inherits styling from before the overlay
|
|
524
|
-
pooledStyleTracker.clear();
|
|
525
|
-
|
|
526
|
-
while (i < line.length) {
|
|
527
|
-
const ansi = extractAnsiCode(line, i);
|
|
528
|
-
if (ansi) {
|
|
529
|
-
// Track all SGR codes to know styling state at afterStart
|
|
530
|
-
pooledStyleTracker.process(ansi.code);
|
|
531
|
-
// Include ANSI codes in their respective segments
|
|
532
|
-
if (currentCol < beforeEnd) {
|
|
533
|
-
pendingAnsiBefore += ansi.code;
|
|
534
|
-
} else if (currentCol >= afterStart && currentCol < afterEnd && afterStarted) {
|
|
535
|
-
// Only include after we've started "after" (styling already prepended)
|
|
536
|
-
after += ansi.code;
|
|
537
|
-
}
|
|
538
|
-
i += ansi.length;
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
let textEnd = i;
|
|
543
|
-
while (textEnd < line.length && !extractAnsiCode(line, textEnd)) textEnd++;
|
|
544
|
-
|
|
545
|
-
for (const { segment } of segmenter.segment(line.slice(i, textEnd))) {
|
|
546
|
-
const w = visibleWidth(segment);
|
|
547
|
-
|
|
548
|
-
if (currentCol < beforeEnd) {
|
|
549
|
-
if (pendingAnsiBefore) {
|
|
550
|
-
before += pendingAnsiBefore;
|
|
551
|
-
pendingAnsiBefore = "";
|
|
552
|
-
}
|
|
553
|
-
before += segment;
|
|
554
|
-
beforeWidth += w;
|
|
555
|
-
} else if (currentCol >= afterStart && currentCol < afterEnd) {
|
|
556
|
-
const fits = !strictAfter || currentCol + w <= afterEnd;
|
|
557
|
-
if (fits) {
|
|
558
|
-
// On first "after" grapheme, prepend inherited styling from before overlay
|
|
559
|
-
if (!afterStarted) {
|
|
560
|
-
after += pooledStyleTracker.getActiveCodes();
|
|
561
|
-
afterStarted = true;
|
|
562
|
-
}
|
|
563
|
-
after += segment;
|
|
564
|
-
afterWidth += w;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
currentCol += w;
|
|
569
|
-
// Early exit: done with "before" only, or done with both segments
|
|
570
|
-
if (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd) break;
|
|
571
|
-
}
|
|
572
|
-
i = textEnd;
|
|
573
|
-
if (afterLen <= 0 ? currentCol >= beforeEnd : currentCol >= afterEnd) break;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return { before, beforeWidth, after, afterWidth };
|
|
211
|
+
return nativeExtractSegments(line, beforeEnd, afterStart, afterLen, strictAfter);
|
|
577
212
|
}
|