@oxecli/oxe 1.0.115 → 1.0.116
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/ui.js +115 -32
- package/package.json +1 -1
package/dist/ui.js
CHANGED
|
@@ -140,11 +140,85 @@ export const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b
|
|
|
140
140
|
export function stripAnsi(s) {
|
|
141
141
|
return s.replace(ANSI_RE, "");
|
|
142
142
|
}
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Display width (wcwidth-style) helpers
|
|
145
|
+
//
|
|
146
|
+
// plainLen() historically counted JS characters, but wide characters (CJK,
|
|
147
|
+
// fullwidth forms, emoji) occupy two terminal columns while counting as one
|
|
148
|
+
// string character. That made table columns, panel borders, wrapping and
|
|
149
|
+
// truncation overflow by the number of wide glyphs. These helpers measure
|
|
150
|
+
// real terminal display width so every frame stays aligned.
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
/** Wide (double-width) code-point ranges. */
|
|
153
|
+
const WIDE_RANGES = [
|
|
154
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
155
|
+
[0x2e80, 0xa4cf], // CJK radicals, Kangxi, CJK symbols, Hiragana/Katakana, Yi
|
|
156
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
157
|
+
[0xf900, 0xfaff], // CJK compatibility
|
|
158
|
+
[0xfe30, 0xfe4f], // CJK compatibility forms
|
|
159
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
160
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
161
|
+
[0x1f300, 0x1f64f], // emoji / misc pictographs
|
|
162
|
+
[0x1f680, 0x1f6ff], // transport & map symbols
|
|
163
|
+
[0x1f900, 0x1f9ff], // supplemental symbols & pictographs
|
|
164
|
+
[0x1fa70, 0x1faff], // symbols & pictographs extended-A
|
|
165
|
+
[0x20000, 0x3fffd], // CJK extension B+ (astral)
|
|
166
|
+
];
|
|
167
|
+
const WIDE_POINTS = new Set([
|
|
168
|
+
0x231a, 0x231b, 0x23e9, 0x23ea, 0x23eb, 0x23ec, 0x23f0, 0x23f3, 0x25fd,
|
|
169
|
+
0x25fe, 0x2614, 0x2615, 0x2648, 0x2649, 0x264a, 0x264b, 0x264c, 0x264d,
|
|
170
|
+
0x264e, 0x264f, 0x2650, 0x2651, 0x2652, 0x2653, 0x267f, 0x2693, 0x26a1,
|
|
171
|
+
0x26aa, 0x26ab, 0x26bd, 0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26d4, 0x26ea,
|
|
172
|
+
0x26f2, 0x26f3, 0x26f5, 0x26fa, 0x26fd, 0x2705, 0x270a, 0x270b, 0x2728,
|
|
173
|
+
0x274c, 0x274e, 0x2753, 0x2754, 0x2755, 0x2757, 0x2795, 0x2796, 0x2797,
|
|
174
|
+
0x27b0, 0x27bf, 0x2b1b, 0x2b1c, 0x2b50, 0x2b55, 0x2329, 0x232a, 0x303f,
|
|
175
|
+
]);
|
|
176
|
+
/** Terminal columns occupied by a single character (0 combining, 1 normal,
|
|
177
|
+
* 2 wide). */
|
|
178
|
+
export function charWidth(ch) {
|
|
179
|
+
const c = ch.codePointAt(0);
|
|
180
|
+
if (c == null)
|
|
181
|
+
return 0;
|
|
182
|
+
// Combining marks occupy no columns of their own.
|
|
183
|
+
if ((c >= 0x0300 && c <= 0x036f) ||
|
|
184
|
+
(c >= 0x1ab0 && c <= 0x1aff) ||
|
|
185
|
+
(c >= 0x1dc0 && c <= 0x1dff) ||
|
|
186
|
+
(c >= 0x20d0 && c <= 0x20ff) ||
|
|
187
|
+
(c >= 0xfe20 && c <= 0xfe2f))
|
|
188
|
+
return 0;
|
|
189
|
+
for (const [lo, hi] of WIDE_RANGES)
|
|
190
|
+
if (c >= lo && c <= hi)
|
|
191
|
+
return 2;
|
|
192
|
+
if (WIDE_POINTS.has(c))
|
|
193
|
+
return 2;
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
/** Terminal display width of a plain (ANSI-stripped) string. */
|
|
197
|
+
export function dispLen(s) {
|
|
198
|
+
let w = 0;
|
|
199
|
+
for (const ch of s)
|
|
200
|
+
w += charWidth(ch);
|
|
201
|
+
return w;
|
|
202
|
+
}
|
|
143
203
|
export function plainLen(s) {
|
|
144
|
-
return stripAnsi(s)
|
|
204
|
+
return dispLen(stripAnsi(s));
|
|
145
205
|
}
|
|
146
|
-
|
|
147
|
-
|
|
206
|
+
/** Take as many characters as fit within maxW terminal columns, never cutting a
|
|
207
|
+
* wide glyph in half. */
|
|
208
|
+
function takeVisible(s, maxW) {
|
|
209
|
+
let w = 0;
|
|
210
|
+
let out = "";
|
|
211
|
+
for (const ch of s) {
|
|
212
|
+
const cw = charWidth(ch);
|
|
213
|
+
if (w + cw > maxW)
|
|
214
|
+
break;
|
|
215
|
+
out += ch;
|
|
216
|
+
w += cw;
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
// Truncate styled text to a max number of visible terminal columns while
|
|
221
|
+
// keeping ANSI escape sequences intact (never cut mid-sequence).
|
|
148
222
|
export function truncateStyled(text, maxVisible) {
|
|
149
223
|
if (maxVisible <= 0)
|
|
150
224
|
return "";
|
|
@@ -159,11 +233,9 @@ export function truncateStyled(text, maxVisible) {
|
|
|
159
233
|
while ((m = re.exec(text)) !== null) {
|
|
160
234
|
if (m.index > last) {
|
|
161
235
|
const seg = text.slice(last, m.index);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
out += seg.slice(0, room);
|
|
166
|
-
visible += Math.min(seg.length, room);
|
|
236
|
+
const taken = takeVisible(seg, maxVisible - visible);
|
|
237
|
+
out += taken;
|
|
238
|
+
visible += dispLen(taken);
|
|
167
239
|
if (visible >= maxVisible)
|
|
168
240
|
return out;
|
|
169
241
|
}
|
|
@@ -171,7 +243,7 @@ export function truncateStyled(text, maxVisible) {
|
|
|
171
243
|
last = m.index + m[0].length;
|
|
172
244
|
}
|
|
173
245
|
if (visible < maxVisible) {
|
|
174
|
-
out += text.slice(last,
|
|
246
|
+
out += takeVisible(text.slice(last), maxVisible - visible);
|
|
175
247
|
}
|
|
176
248
|
return out;
|
|
177
249
|
}
|
|
@@ -234,11 +306,11 @@ function wrapStyledToWidth(line, width) {
|
|
|
234
306
|
const isSpace = /\s/.test(a.ch);
|
|
235
307
|
if (isSpace) {
|
|
236
308
|
cur.push(a);
|
|
237
|
-
curLen
|
|
309
|
+
curLen += charWidth(a.ch);
|
|
238
310
|
breakAt = cur.length - 1;
|
|
239
311
|
continue;
|
|
240
312
|
}
|
|
241
|
-
if (curLen +
|
|
313
|
+
if (curLen + charWidth(a.ch) > width) {
|
|
242
314
|
if (breakAt >= 0) {
|
|
243
315
|
// Break after the whitespace at breakAt: keep the whitespace's ANSI
|
|
244
316
|
// prefix (often a reset) so style never leaks onto the continuation.
|
|
@@ -260,7 +332,7 @@ function wrapStyledToWidth(line, width) {
|
|
|
260
332
|
}
|
|
261
333
|
}
|
|
262
334
|
cur.push(a);
|
|
263
|
-
curLen
|
|
335
|
+
curLen += charWidth(a.ch);
|
|
264
336
|
}
|
|
265
337
|
if (cur.length)
|
|
266
338
|
rows.push(cur);
|
|
@@ -565,7 +637,7 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
565
637
|
const plainLines = styledLines.map((l) => stripAnsi(l));
|
|
566
638
|
let maxLineW = 0;
|
|
567
639
|
for (const p of plainLines)
|
|
568
|
-
maxLineW = Math.max(maxLineW, p
|
|
640
|
+
maxLineW = Math.max(maxLineW, plainLen(p));
|
|
569
641
|
if (title)
|
|
570
642
|
maxLineW = Math.max(maxLineW, plainLen(title) + 4);
|
|
571
643
|
if (subtitle)
|
|
@@ -599,8 +671,8 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
|
|
|
599
671
|
for (let i = 0; i < styledLines.length; i++) {
|
|
600
672
|
const line = styledLines[i];
|
|
601
673
|
const plain = plainLines[i] ?? "";
|
|
602
|
-
const pad = Math.max(innerW - plain
|
|
603
|
-
const display = plain
|
|
674
|
+
const pad = Math.max(innerW - plainLen(plain), 0);
|
|
675
|
+
const display = plainLen(plain) > innerW ? truncateStyled(line, innerW) : line;
|
|
604
676
|
out.push(`\x1b[${borderStyle}m│\x1b[0m ${display}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
|
|
605
677
|
}
|
|
606
678
|
const bottom = `╰${embed(subtitle, "center")}╯`;
|
|
@@ -715,7 +787,7 @@ export function displayRows(text) {
|
|
|
715
787
|
if (!lines.length)
|
|
716
788
|
return 1;
|
|
717
789
|
for (const line of lines) {
|
|
718
|
-
rows += Math.max(1, Math.ceil(line
|
|
790
|
+
rows += Math.max(1, Math.ceil(dispLen(line) / width));
|
|
719
791
|
}
|
|
720
792
|
return rows;
|
|
721
793
|
}
|
|
@@ -1084,12 +1156,12 @@ export class Spinner {
|
|
|
1084
1156
|
const first = parts[0];
|
|
1085
1157
|
const rest = parts.slice(1);
|
|
1086
1158
|
const plain = stripAnsi(first);
|
|
1087
|
-
const pad = Math.max(0, this.lastLen - plain
|
|
1159
|
+
const pad = Math.max(0, this.lastLen - plainLen(plain));
|
|
1088
1160
|
process.stdout.write("\r" + first + " ".repeat(pad) + "\r\n");
|
|
1089
1161
|
if (rest.length)
|
|
1090
1162
|
process.stdout.write(rest.join("\n") + "\n");
|
|
1091
1163
|
this.rows = 0;
|
|
1092
|
-
this.lastLen = plain
|
|
1164
|
+
this.lastLen = plainLen(plain);
|
|
1093
1165
|
}
|
|
1094
1166
|
update(text) {
|
|
1095
1167
|
if (this.timer) {
|
|
@@ -1113,9 +1185,9 @@ export class Spinner {
|
|
|
1113
1185
|
// cells (spinner frame, trailing timer) actually change on screen. Pad
|
|
1114
1186
|
// with spaces so a shorter line clears any previously longer text.
|
|
1115
1187
|
const plain = stripAnsi(rendered);
|
|
1116
|
-
const pad = Math.max(0, this.lastLen - plain
|
|
1188
|
+
const pad = Math.max(0, this.lastLen - plainLen(plain));
|
|
1117
1189
|
process.stdout.write("\r" + rendered + " ".repeat(pad) + "\r");
|
|
1118
|
-
this.lastLen = plain
|
|
1190
|
+
this.lastLen = plainLen(plain);
|
|
1119
1191
|
}
|
|
1120
1192
|
else {
|
|
1121
1193
|
// Multi-row wrapped text: fall back to an erase-based redraw.
|
|
@@ -1379,7 +1451,7 @@ function wrapRuns(runs, width) {
|
|
|
1379
1451
|
caretCol = lineLen;
|
|
1380
1452
|
}
|
|
1381
1453
|
line.push(u);
|
|
1382
|
-
lineLen +=
|
|
1454
|
+
lineLen += charWidth(u.ch);
|
|
1383
1455
|
};
|
|
1384
1456
|
let i = 0;
|
|
1385
1457
|
while (i < units.length) {
|
|
@@ -1398,7 +1470,8 @@ function wrapRuns(runs, width) {
|
|
|
1398
1470
|
continue;
|
|
1399
1471
|
}
|
|
1400
1472
|
if (isWs(u)) {
|
|
1401
|
-
|
|
1473
|
+
const w = charWidth(u.ch);
|
|
1474
|
+
if (lineLen + w > width)
|
|
1402
1475
|
flushLine();
|
|
1403
1476
|
// Never start a line with whitespace: if the space lands at the start of
|
|
1404
1477
|
// a fresh line, drop it (it was the fill of the previous full line).
|
|
@@ -1413,28 +1486,38 @@ function wrapRuns(runs, width) {
|
|
|
1413
1486
|
// Start of a word (which may contain the caret). Find its full extent in
|
|
1414
1487
|
// the flattened stream so we never wrap mid-word.
|
|
1415
1488
|
let j = i;
|
|
1416
|
-
let
|
|
1489
|
+
let wordW = 0; // display width of the whole word
|
|
1417
1490
|
while (j < units.length && !isWs(units[j])) {
|
|
1418
|
-
|
|
1491
|
+
wordW += charWidth(units[j].ch);
|
|
1419
1492
|
j++;
|
|
1420
1493
|
}
|
|
1421
|
-
if (
|
|
1494
|
+
if (wordW === 0) {
|
|
1422
1495
|
i++;
|
|
1423
1496
|
continue;
|
|
1424
1497
|
}
|
|
1425
1498
|
if (lineLen === 0) {
|
|
1426
|
-
if (
|
|
1499
|
+
if (wordW > width) {
|
|
1427
1500
|
// Overlong word: split into width-sized chunks, keeping the caret in
|
|
1428
1501
|
// the chunk that contains it.
|
|
1429
1502
|
let from = i;
|
|
1430
|
-
let
|
|
1431
|
-
while (
|
|
1432
|
-
|
|
1503
|
+
let used = 0;
|
|
1504
|
+
while (used < wordW) {
|
|
1505
|
+
let n = 0;
|
|
1506
|
+
let chunkW = 0;
|
|
1507
|
+
while (from + n < j && chunkW + charWidth(units[from + n].ch) <= width) {
|
|
1508
|
+
chunkW += charWidth(units[from + n].ch);
|
|
1509
|
+
n++;
|
|
1510
|
+
}
|
|
1511
|
+
if (n === 0) {
|
|
1512
|
+
// A single glyph is wider than the line; emit it alone.
|
|
1513
|
+
n = 1;
|
|
1514
|
+
chunkW = charWidth(units[from].ch);
|
|
1515
|
+
}
|
|
1433
1516
|
for (let k = 0; k < n; k++)
|
|
1434
1517
|
addUnit(units[from + k]);
|
|
1435
1518
|
flushLine();
|
|
1436
1519
|
from += n;
|
|
1437
|
-
|
|
1520
|
+
used += chunkW;
|
|
1438
1521
|
}
|
|
1439
1522
|
}
|
|
1440
1523
|
else {
|
|
@@ -1444,7 +1527,7 @@ function wrapRuns(runs, width) {
|
|
|
1444
1527
|
i = j;
|
|
1445
1528
|
}
|
|
1446
1529
|
else {
|
|
1447
|
-
if (lineLen +
|
|
1530
|
+
if (lineLen + wordW > width) {
|
|
1448
1531
|
// Word doesn't fit on the current line; move the whole word to a new
|
|
1449
1532
|
// line (leaving any trailing space from the previous word behind).
|
|
1450
1533
|
flushLine();
|
|
@@ -1565,7 +1648,7 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1565
1648
|
const body = [];
|
|
1566
1649
|
for (const l of rows) {
|
|
1567
1650
|
const plain = stripAnsi(l);
|
|
1568
|
-
const pad = Math.max(0, innerW - plain
|
|
1651
|
+
const pad = Math.max(0, innerW - plainLen(plain));
|
|
1569
1652
|
body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
|
|
1570
1653
|
}
|
|
1571
1654
|
const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
|