@oh-my-pi/pi-tui 18.4.1 → 18.4.2
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/CHANGELOG.md +14 -0
- package/dist/types/chrome/transcript-container.d.ts +15 -1
- package/dist/types/overlays/model-browser.d.ts +15 -0
- package/dist/types/overlays/rewind-selector.d.ts +2 -1
- package/dist/types/prompt/composer-attachments.d.ts +7 -0
- package/dist/types/prompt/model-mention-autocomplete.d.ts +4 -1
- package/dist/types/render/width-aware-text.d.ts +6 -0
- package/dist/types/terminal.d.ts +7 -0
- package/dist/types/theme/color.d.ts +1 -0
- package/dist/types/theme/session-color.d.ts +3 -3
- package/dist/types/theme/shimmer.d.ts +1 -1
- package/dist/types/theme/theme-class.d.ts +1 -1
- package/dist/types/tui.d.ts +3 -2
- package/package.json +9 -9
- package/src/chat/tool-execution.ts +24 -4
- package/src/chrome/transcript-container.ts +76 -14
- package/src/components/editor.ts +28 -6
- package/src/components/image.ts +15 -7
- package/src/components/loader.ts +37 -17
- package/src/overlays/agent-hub.ts +16 -6
- package/src/overlays/copy-selector.ts +5 -0
- package/src/overlays/model-browser.ts +106 -25
- package/src/overlays/rewind-selector.ts +10 -2
- package/src/prompt/composer-attachments.ts +54 -7
- package/src/prompt/composer.ts +5 -2
- package/src/prompt/custom-editor.ts +24 -6
- package/src/prompt/model-mention-autocomplete.ts +22 -10
- package/src/render/width-aware-text.ts +9 -0
- package/src/status-line/component.ts +213 -132
- package/src/status-line/segments.ts +34 -29
- package/src/terminal.ts +37 -11
- package/src/theme/color.ts +15 -0
- package/src/theme/session-color.ts +26 -4
- package/src/theme/shimmer.ts +55 -76
- package/src/theme/theme-class.ts +29 -12
- package/src/tui.ts +142 -112
- package/src/utils.ts +15 -0
package/src/tui.ts
CHANGED
|
@@ -604,100 +604,95 @@ function isSgrParamByte(c: number): boolean {
|
|
|
604
604
|
// standalone fg-red), changing the rendered color. The self-delimiting colon
|
|
605
605
|
// form (`38:2::r:g:b`) is unambiguous — its tokens never equal a bare `38`, so
|
|
606
606
|
// the scan treats it as a complete unit and merging stays safe.
|
|
607
|
-
function endsWithIncompleteExtendedColor(
|
|
608
|
-
|
|
609
|
-
let
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
607
|
+
function endsWithIncompleteExtendedColor(line: string, start: number, end: number): boolean {
|
|
608
|
+
let tokenStart = start;
|
|
609
|
+
let needsMode = false;
|
|
610
|
+
let valuesRemaining = 0;
|
|
611
|
+
for (let i = start; i <= end; i++) {
|
|
612
|
+
if (i !== end && line.charCodeAt(i) !== CC_SEMI) continue;
|
|
613
|
+
const tokenLength = i - tokenStart;
|
|
614
|
+
const first = line.charCodeAt(tokenStart);
|
|
615
|
+
if (valuesRemaining > 0) {
|
|
616
|
+
valuesRemaining--;
|
|
617
|
+
} else if (needsMode && tokenLength === 1 && (first === 0x32 || first === 0x35)) {
|
|
618
|
+
valuesRemaining = first === 0x32 ? 3 : 1;
|
|
619
|
+
needsMode = false;
|
|
620
|
+
} else {
|
|
621
|
+
// An unrecognized mode is also a standalone token: `38;48;5`
|
|
622
|
+
// still ends in an incomplete background palette spec.
|
|
623
|
+
needsMode = tokenLength === 2 && first >= 0x33 && first <= 0x35 && line.charCodeAt(tokenStart + 1) === 0x38;
|
|
625
624
|
}
|
|
626
|
-
i
|
|
625
|
+
tokenStart = i + 1;
|
|
627
626
|
}
|
|
628
|
-
return
|
|
627
|
+
return needsMode || valuesRemaining > 0;
|
|
629
628
|
}
|
|
630
629
|
|
|
631
630
|
/**
|
|
632
631
|
* Merge runs of byte-adjacent SGR sequences (`CSI [0-9;:]* m`) into one. Only
|
|
633
632
|
* CSI-SGR sequences are touched; text, cursor moves, OSC, hyperlinks and image
|
|
634
|
-
* payloads pass through verbatim.
|
|
635
|
-
*
|
|
633
|
+
* payloads pass through verbatim. Isolated sequences require no parameter
|
|
634
|
+
* slices or arrays; output is built only at merged boundaries or empty resets
|
|
635
|
+
* that need normalization within an adjacent run.
|
|
636
636
|
*/
|
|
637
637
|
export function coalesceAdjacentSgr(line: string): string {
|
|
638
|
-
if (!SGR_COALESCE_ENABLED
|
|
638
|
+
if (!SGR_COALESCE_ENABLED) return line;
|
|
639
639
|
const n = line.length;
|
|
640
640
|
let out = "";
|
|
641
641
|
let copiedUpto = 0;
|
|
642
|
-
let i =
|
|
643
|
-
while (i
|
|
644
|
-
if (line.charCodeAt(i) !== CC_ESC || line.charCodeAt(i + 1) !== CC_BRACKET) {
|
|
645
|
-
i++;
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
642
|
+
let i = line.indexOf("\x1b[");
|
|
643
|
+
while (i !== -1) {
|
|
648
644
|
// Scan a candidate SGR sequence: ESC [ <params> m.
|
|
649
|
-
let
|
|
650
|
-
|
|
645
|
+
let start = i + 2;
|
|
646
|
+
let j = start;
|
|
647
|
+
let groupTokens = 1;
|
|
648
|
+
while (j < n && isSgrParamByte(line.charCodeAt(j))) {
|
|
649
|
+
const cc = line.charCodeAt(j++);
|
|
650
|
+
if (cc === CC_SEMI || cc === CC_COLON) groupTokens++;
|
|
651
|
+
}
|
|
651
652
|
if (j >= n || line.charCodeAt(j) !== CC_M) {
|
|
652
653
|
// Not an SGR (e.g. cursor move); leave it in the pending region.
|
|
653
|
-
i = j;
|
|
654
|
+
i = line.indexOf("\x1b[", j);
|
|
654
655
|
continue;
|
|
655
656
|
}
|
|
656
|
-
|
|
657
|
-
|
|
657
|
+
|
|
658
|
+
let adjacent = false;
|
|
658
659
|
let k = j + 1;
|
|
659
660
|
while (k < n && line.charCodeAt(k) === CC_ESC && line.charCodeAt(k + 1) === CC_BRACKET) {
|
|
660
|
-
|
|
661
|
-
|
|
661
|
+
const nextStart = k + 2;
|
|
662
|
+
let p = nextStart;
|
|
663
|
+
let tokens = 1;
|
|
664
|
+
while (p < n && isSgrParamByte(line.charCodeAt(p))) {
|
|
665
|
+
const cc = line.charCodeAt(p++);
|
|
666
|
+
if (cc === CC_SEMI || cc === CC_COLON) tokens++;
|
|
667
|
+
}
|
|
662
668
|
if (p >= n || line.charCodeAt(p) !== CC_M) break;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
let group = "";
|
|
679
|
-
let groupTokens = 0;
|
|
680
|
-
let groupOpenSafe = true;
|
|
681
|
-
for (let q = 0; q < params.length; q++) {
|
|
682
|
-
const norm = params[q]!.length === 0 ? "0" : params[q]!;
|
|
683
|
-
let tk = 1;
|
|
684
|
-
for (let z = 0; z < norm.length; z++) {
|
|
685
|
-
const cc = norm.charCodeAt(z);
|
|
686
|
-
if (cc === CC_SEMI || cc === CC_COLON) tk++;
|
|
687
|
-
}
|
|
688
|
-
if (groupTokens > 0 && (!groupOpenSafe || groupTokens + tk > MERGE_TOKEN_CAP)) {
|
|
689
|
-
out += `\x1b[${group}m`;
|
|
690
|
-
group = "";
|
|
691
|
-
groupTokens = 0;
|
|
669
|
+
adjacent = true;
|
|
670
|
+
|
|
671
|
+
// Keep the boundary if the preceding list could absorb a missing
|
|
672
|
+
// channel/index, or if merging would overflow the parameter cap.
|
|
673
|
+
// Otherwise replace only `m ESC [` with `;`, keeping the source
|
|
674
|
+
// untouched until a boundary actually merges. Empty lists in an
|
|
675
|
+
// adjacent run normalize to `0`, including at a guarded boundary.
|
|
676
|
+
if (groupTokens + tokens <= MERGE_TOKEN_CAP && !endsWithIncompleteExtendedColor(line, start, j)) {
|
|
677
|
+
out += line.slice(copiedUpto, j) + (start === j ? "0;" : ";");
|
|
678
|
+
copiedUpto = nextStart;
|
|
679
|
+
groupTokens += tokens;
|
|
680
|
+
} else {
|
|
681
|
+
if (start === j) {
|
|
682
|
+
out += `${line.slice(copiedUpto, j)}0`;
|
|
683
|
+
copiedUpto = j;
|
|
692
684
|
}
|
|
693
|
-
|
|
694
|
-
groupTokens += tk;
|
|
695
|
-
groupOpenSafe = !endsWithIncompleteExtendedColor(norm);
|
|
685
|
+
groupTokens = tokens;
|
|
696
686
|
}
|
|
697
|
-
|
|
698
|
-
|
|
687
|
+
start = nextStart;
|
|
688
|
+
j = p;
|
|
689
|
+
k = p + 1;
|
|
699
690
|
}
|
|
700
|
-
|
|
691
|
+
if (adjacent && start === j) {
|
|
692
|
+
out += `${line.slice(copiedUpto, j)}0`;
|
|
693
|
+
copiedUpto = j;
|
|
694
|
+
}
|
|
695
|
+
i = line.indexOf("\x1b[", k);
|
|
701
696
|
}
|
|
702
697
|
if (copiedUpto === 0) return line;
|
|
703
698
|
return out + line.slice(copiedUpto);
|
|
@@ -776,6 +771,12 @@ export class TUI extends Container {
|
|
|
776
771
|
// the established differential comparison and resize accounting.
|
|
777
772
|
#providerWindow: string[] = [];
|
|
778
773
|
#providerPreparedRows: PreparedLine[] = [];
|
|
774
|
+
// Rows of the last marker-stripping prepare pass keyed by their stripped
|
|
775
|
+
// raw line, so rows that only moved (a scroll or a history commit shifts
|
|
776
|
+
// every row under the positional sidecar) reuse their preparation. Swapped
|
|
777
|
+
// with the spare each pass, which bounds the memo to one frame of rows.
|
|
778
|
+
#preparedLineMemo = new Map<string, PreparedLine>();
|
|
779
|
+
#preparedLineMemoSpare = new Map<string, PreparedLine>();
|
|
779
780
|
#previousFrameLength = 0;
|
|
780
781
|
#previousWidth = 0;
|
|
781
782
|
#previousHeight = 0;
|
|
@@ -1762,9 +1763,13 @@ export class TUI extends Container {
|
|
|
1762
1763
|
(provider ? provider.renderFrame({ columns: width, rows: height }).viewport : this.render(width));
|
|
1763
1764
|
} while (this.#imageBudget.endPass());
|
|
1764
1765
|
const viewport = rendered.length > height ? rendered.slice(rendered.length - height) : Array.from(rendered);
|
|
1765
|
-
this.#extractCursorMarkers(viewport);
|
|
1766
1766
|
// The borrowed resize buffer is transient, not a streamable session paint.
|
|
1767
|
-
this.#emitAltFrame(
|
|
1767
|
+
this.#emitAltFrame(
|
|
1768
|
+
this.#prepareLinesArray(viewport, width, this.#altPreparedRows, height, []),
|
|
1769
|
+
width,
|
|
1770
|
+
height,
|
|
1771
|
+
false,
|
|
1772
|
+
);
|
|
1768
1773
|
}
|
|
1769
1774
|
|
|
1770
1775
|
/**
|
|
@@ -2586,30 +2591,6 @@ export class TUI extends Container {
|
|
|
2586
2591
|
return sliceByColumn(result, 0, totalWidth, true);
|
|
2587
2592
|
}
|
|
2588
2593
|
|
|
2589
|
-
/**
|
|
2590
|
-
* Strip every CURSOR_MARKER from the rendered lines (markers are internal
|
|
2591
|
-
* sentinels and must never reach the terminal) and return their positions,
|
|
2592
|
-
* bottom-most first. Callers pick the visible one once the window top is
|
|
2593
|
-
* known.
|
|
2594
|
-
*/
|
|
2595
|
-
#extractCursorMarkers(lines: string[]): { row: number; col: number }[] {
|
|
2596
|
-
const markers: { row: number; col: number }[] = [];
|
|
2597
|
-
for (let row = lines.length - 1; row >= 0; row--) {
|
|
2598
|
-
const line = lines[row];
|
|
2599
|
-
let markerIndex = line.indexOf(CURSOR_MARKER);
|
|
2600
|
-
if (markerIndex === -1) continue;
|
|
2601
|
-
const beforeMarker = line.slice(0, markerIndex);
|
|
2602
|
-
markers.push({ row, col: visibleWidth(beforeMarker) });
|
|
2603
|
-
let stripped = line;
|
|
2604
|
-
while (markerIndex !== -1) {
|
|
2605
|
-
stripped = stripped.slice(0, markerIndex) + stripped.slice(markerIndex + CURSOR_MARKER.length);
|
|
2606
|
-
markerIndex = stripped.indexOf(CURSOR_MARKER, markerIndex);
|
|
2607
|
-
}
|
|
2608
|
-
lines[row] = stripped;
|
|
2609
|
-
}
|
|
2610
|
-
return markers;
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
2594
|
/**
|
|
2614
2595
|
* Rewrite a Kitty direct-placement line for the viewport row it is written
|
|
2615
2596
|
* at, clipping to the visible slice (see {@link encodeKittyPlacementLine})
|
|
@@ -2802,9 +2783,11 @@ export class TUI extends Container {
|
|
|
2802
2783
|
replayViewportRows = moved;
|
|
2803
2784
|
}
|
|
2804
2785
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2786
|
+
// History first: it reuses the previous viewport's rows by content, and
|
|
2787
|
+
// the viewport pass replaces that memo with its own rows.
|
|
2807
2788
|
const preparedHistory = this.#prepareLinesArray(historyRows, width);
|
|
2789
|
+
const markers: { row: number; col: number }[] = [];
|
|
2790
|
+
const prepared = this.#prepareLinesArray(viewport, width, this.#providerPreparedRows, viewport.length, markers);
|
|
2808
2791
|
const rows = prepared.lines.length;
|
|
2809
2792
|
// Destructive reset (session replace, /tree, explicit clear, or a settled
|
|
2810
2793
|
// resize in rebuild mode): erase native history and the viewport,
|
|
@@ -3148,13 +3131,23 @@ export class TUI extends Container {
|
|
|
3148
3131
|
* Prepare one string projection plus its structured write sidecar. A prior
|
|
3149
3132
|
* sidecar entry is reusable only under identical raw content, width, width
|
|
3150
3133
|
* configuration, and image protocol; those are every mutable input to
|
|
3151
|
-
* normalization, fitting, classification, and terminal coalescing.
|
|
3134
|
+
* normalization, fitting, classification, and terminal coalescing. The same
|
|
3135
|
+
* row at the same index is checked first, then the previous stripping pass's
|
|
3136
|
+
* rows by content, so a scrolled row is not re-prepared.
|
|
3137
|
+
*
|
|
3138
|
+
* With `markers`, every CURSOR_MARKER is stripped (markers are internal
|
|
3139
|
+
* sentinels and must never reach the terminal) and its position recorded,
|
|
3140
|
+
* bottom-most first; callers pick the visible one once the window top is
|
|
3141
|
+
* known. Without it (history rows) lines are prepared verbatim. Every
|
|
3142
|
+
* reusable entry holds a stripped raw line, so a row that matches one
|
|
3143
|
+
* carries no marker and skips the marker scan.
|
|
3152
3144
|
*/
|
|
3153
3145
|
#prepareLinesArray(
|
|
3154
3146
|
lines: readonly string[],
|
|
3155
3147
|
width: number,
|
|
3156
3148
|
previous: readonly PreparedLine[] = [],
|
|
3157
3149
|
length = lines.length,
|
|
3150
|
+
markers?: { row: number; col: number }[],
|
|
3158
3151
|
): PreparedLines {
|
|
3159
3152
|
// oxlint-disable-next-line unicorn/no-new-array -- render-frame length preallocation
|
|
3160
3153
|
const prepared: string[] = new Array(length);
|
|
@@ -3162,23 +3155,61 @@ export class TUI extends Container {
|
|
|
3162
3155
|
const rows: PreparedLine[] = new Array(length);
|
|
3163
3156
|
const widthEpoch = getWidthConfigEpoch();
|
|
3164
3157
|
const imageProtocol = TERMINAL.imageProtocol;
|
|
3158
|
+
const memo = this.#preparedLineMemo;
|
|
3159
|
+
// Only stripping passes index their rows: a verbatim history row may keep
|
|
3160
|
+
// a marker, and reusing it for a viewport row would leak that marker.
|
|
3161
|
+
const nextMemo = markers === undefined ? undefined : this.#preparedLineMemoSpare;
|
|
3165
3162
|
for (let i = 0; i < length; i++) {
|
|
3166
|
-
const
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3163
|
+
const source = lines[i] ?? "";
|
|
3164
|
+
let row = this.#reusablePreparedLine(previous[i], memo, source, width, widthEpoch, imageProtocol);
|
|
3165
|
+
if (row === undefined) {
|
|
3166
|
+
let raw = source;
|
|
3167
|
+
if (markers !== undefined) {
|
|
3168
|
+
let markerIndex = source.indexOf(CURSOR_MARKER);
|
|
3169
|
+
if (markerIndex !== -1) {
|
|
3170
|
+
markers.push({ row: i, col: visibleWidth(source.slice(0, markerIndex)) });
|
|
3171
|
+
// Resume the search just before the splice so a marker that the
|
|
3172
|
+
// removal itself joins together is stripped too; reusable rows
|
|
3173
|
+
// must stay marker-free.
|
|
3174
|
+
while (markerIndex !== -1) {
|
|
3175
|
+
raw = raw.slice(0, markerIndex) + raw.slice(markerIndex + CURSOR_MARKER.length);
|
|
3176
|
+
markerIndex = raw.indexOf(CURSOR_MARKER, Math.max(0, markerIndex - CURSOR_MARKER.length + 1));
|
|
3177
|
+
}
|
|
3178
|
+
row = this.#reusablePreparedLine(previous[i], memo, raw, width, widthEpoch, imageProtocol);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
row ??= this.#prepareLine(raw, width, widthEpoch, imageProtocol);
|
|
3182
|
+
}
|
|
3183
|
+
nextMemo?.set(row.raw, row);
|
|
3176
3184
|
prepared[i] = row.line;
|
|
3177
3185
|
rows[i] = row;
|
|
3178
3186
|
}
|
|
3187
|
+
if (nextMemo !== undefined) {
|
|
3188
|
+
memo.clear();
|
|
3189
|
+
this.#preparedLineMemo = nextMemo;
|
|
3190
|
+
this.#preparedLineMemoSpare = memo;
|
|
3191
|
+
markers?.reverse();
|
|
3192
|
+
}
|
|
3179
3193
|
return { lines: prepared, rows };
|
|
3180
3194
|
}
|
|
3181
3195
|
|
|
3196
|
+
#reusablePreparedLine(
|
|
3197
|
+
positional: PreparedLine | undefined,
|
|
3198
|
+
memo: ReadonlyMap<string, PreparedLine>,
|
|
3199
|
+
raw: string,
|
|
3200
|
+
width: number,
|
|
3201
|
+
widthEpoch: number,
|
|
3202
|
+
imageProtocol: ImageProtocol | null,
|
|
3203
|
+
): PreparedLine | undefined {
|
|
3204
|
+
const cached = positional !== undefined && positional.raw === raw ? positional : memo.get(raw);
|
|
3205
|
+
return cached !== undefined &&
|
|
3206
|
+
cached.width === width &&
|
|
3207
|
+
cached.widthEpoch === widthEpoch &&
|
|
3208
|
+
cached.imageProtocol === imageProtocol
|
|
3209
|
+
? cached
|
|
3210
|
+
: undefined;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3182
3213
|
#prepareLine(raw: string, width: number, widthEpoch: number, imageProtocol: ImageProtocol | null): PreparedLine {
|
|
3183
3214
|
const safeWidth = Number.isFinite(width) ? Math.max(1, Math.trunc(width)) : 1;
|
|
3184
3215
|
const maxSourceLength = Math.min(
|
|
@@ -3569,8 +3600,7 @@ export class TUI extends Container {
|
|
|
3569
3600
|
this.#imageBudget.beginPass(false, true);
|
|
3570
3601
|
lines = this.#compositeOverlaysIntoWindow(base, width, height);
|
|
3571
3602
|
} while (this.#imageBudget.endPass());
|
|
3572
|
-
this.#
|
|
3573
|
-
const prepared = this.#prepareLinesArray(lines, width, this.#altPreparedRows, height);
|
|
3603
|
+
const prepared = this.#prepareLinesArray(lines, width, this.#altPreparedRows, height, []);
|
|
3574
3604
|
this.#emitAltFrame(prepared, width, height, true);
|
|
3575
3605
|
}
|
|
3576
3606
|
|
package/src/utils.ts
CHANGED
|
@@ -241,6 +241,9 @@ const OSC66_PREFIX = "\x1b]66;";
|
|
|
241
241
|
const APC_SPAN_REGEX = /\x1b_[\s\S]*?(?:\x07|\x1b\\)/g;
|
|
242
242
|
const APC_PREFIX = "\x1b_";
|
|
243
243
|
const PRINTABLE_ASCII_REGEX = /^[\u0020-\u007e]*$/;
|
|
244
|
+
// Keep native escape parsing: a JS SGR parser costs more than Bun's scanner.
|
|
245
|
+
// Test Jamo separately from the ASCII correction markers to keep scans cheap.
|
|
246
|
+
const HANGUL_COMPAT_JAMO_REGEX = /[\u3131-\u318e]/;
|
|
244
247
|
|
|
245
248
|
// Pin Bun.stringWidth semantics to the native width engine and guard against Bun
|
|
246
249
|
// default drift: strip ANSI/OSC (don't count escape bytes) and treat
|
|
@@ -337,6 +340,18 @@ export function visibleWidth(str: string): number {
|
|
|
337
340
|
return str.length;
|
|
338
341
|
}
|
|
339
342
|
|
|
343
|
+
// The extra gate pays for itself on long uncached lines, not short cache
|
|
344
|
+
// misses. APC and OSC 66 still need their existing payload corrections.
|
|
345
|
+
if (
|
|
346
|
+
!cacheable &&
|
|
347
|
+
!str.includes("\t") &&
|
|
348
|
+
!HANGUL_COMPAT_JAMO_REGEX.test(str) &&
|
|
349
|
+
!str.includes(APC_PREFIX) &&
|
|
350
|
+
!str.includes(OSC66_PREFIX)
|
|
351
|
+
) {
|
|
352
|
+
return Bun.stringWidth(str, STRING_WIDTH_OPTS);
|
|
353
|
+
}
|
|
354
|
+
|
|
340
355
|
let tabCount = 0;
|
|
341
356
|
let compatibilityJamoCount = 0;
|
|
342
357
|
let fillerCount = 0;
|