@oxecli/oxe 1.0.113 → 1.0.115
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 +143 -62
- package/package.json +1 -1
package/dist/ui.js
CHANGED
|
@@ -116,8 +116,10 @@ export function clearScreen() {
|
|
|
116
116
|
export function terminalWidth() {
|
|
117
117
|
return process.stdout.columns || 80;
|
|
118
118
|
}
|
|
119
|
-
/** Minimum width for boxes/cards so they don't collapse to nothing.
|
|
120
|
-
|
|
119
|
+
/** Minimum width for boxes/cards so they don't collapse to nothing. Kept
|
|
120
|
+
* well below the smallest terminal width so a card can keep scaling down
|
|
121
|
+
* (it never forces a box wider than the terminal). */
|
|
122
|
+
const MIN_BOX_WIDTH = 30;
|
|
121
123
|
/** Horizontal margin reserved on each side of a box/card from the terminal edge. */
|
|
122
124
|
const BOX_MARGIN = 4;
|
|
123
125
|
/**
|
|
@@ -1336,69 +1338,126 @@ export function splitBlocks(buffer, pasteSpans) {
|
|
|
1336
1338
|
}
|
|
1337
1339
|
const PROMPT_CARET = "\u0000";
|
|
1338
1340
|
function wrapRuns(runs, width) {
|
|
1341
|
+
const units = [];
|
|
1342
|
+
for (const run of runs) {
|
|
1343
|
+
if (run.text === PROMPT_CARET) {
|
|
1344
|
+
units.push({ ch: PROMPT_CARET, style: run.style, caret: true });
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
for (const ch of run.text) {
|
|
1348
|
+
units.push({ ch, style: run.style, caret: false });
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (units.length === 0)
|
|
1353
|
+
units.push({ ch: "", style: "", caret: false });
|
|
1339
1354
|
const rows = [];
|
|
1340
|
-
let
|
|
1341
|
-
let
|
|
1355
|
+
let line = [];
|
|
1356
|
+
let lineLen = 0;
|
|
1342
1357
|
let caretRow = -1;
|
|
1343
1358
|
let caretCol = -1;
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1359
|
+
const flushLine = () => {
|
|
1360
|
+
if (line.length === 0)
|
|
1361
|
+
return;
|
|
1362
|
+
let out = "";
|
|
1363
|
+
for (const u of line) {
|
|
1364
|
+
if (u.caret)
|
|
1365
|
+
out += u.style + "▏" + "\x1b[0m";
|
|
1366
|
+
else if (u.style)
|
|
1367
|
+
out += u.style + u.ch + "\x1b[0m";
|
|
1368
|
+
else
|
|
1369
|
+
out += u.ch;
|
|
1370
|
+
}
|
|
1371
|
+
rows.push(out);
|
|
1372
|
+
line = [];
|
|
1373
|
+
lineLen = 0;
|
|
1348
1374
|
};
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
flush();
|
|
1375
|
+
const isWs = (u) => !u.caret && /\s/.test(u.ch);
|
|
1376
|
+
const addUnit = (u) => {
|
|
1377
|
+
if (u.caret) {
|
|
1353
1378
|
caretRow = rows.length;
|
|
1354
|
-
caretCol =
|
|
1355
|
-
|
|
1356
|
-
|
|
1379
|
+
caretCol = lineLen;
|
|
1380
|
+
}
|
|
1381
|
+
line.push(u);
|
|
1382
|
+
lineLen += 1;
|
|
1383
|
+
};
|
|
1384
|
+
let i = 0;
|
|
1385
|
+
while (i < units.length) {
|
|
1386
|
+
const u = units[i];
|
|
1387
|
+
if (u.caret) {
|
|
1388
|
+
if (lineLen >= width)
|
|
1389
|
+
flushLine();
|
|
1390
|
+
addUnit(u);
|
|
1391
|
+
i++;
|
|
1357
1392
|
continue;
|
|
1358
1393
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
if (take === 0)
|
|
1374
|
-
take = 1;
|
|
1375
|
-
take = Math.min(take, width);
|
|
1376
|
-
}
|
|
1377
|
-
else {
|
|
1378
|
-
// Mid-line: append the next whole word (plus its trailing space) only
|
|
1379
|
-
// if it fits; otherwise move the entire word to the next line.
|
|
1380
|
-
const sp = rest.indexOf(" ");
|
|
1381
|
-
take = sp === -1 ? rest.length : sp + 1;
|
|
1382
|
-
if (take > width - curLen) {
|
|
1383
|
-
flush();
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
const chunk = rest.slice(0, take);
|
|
1388
|
-
cur += run.style ? run.style + chunk + "\x1b[0m" : chunk;
|
|
1389
|
-
curLen += take;
|
|
1390
|
-
rest = rest.slice(take);
|
|
1394
|
+
if (u.ch === "\n") {
|
|
1395
|
+
// Hard line break: end the current row and start a fresh one.
|
|
1396
|
+
flushLine();
|
|
1397
|
+
i++;
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (isWs(u)) {
|
|
1401
|
+
if (lineLen + 1 > width)
|
|
1402
|
+
flushLine();
|
|
1403
|
+
// Never start a line with whitespace: if the space lands at the start of
|
|
1404
|
+
// a fresh line, drop it (it was the fill of the previous full line).
|
|
1405
|
+
if (lineLen === 0) {
|
|
1406
|
+
i++;
|
|
1407
|
+
continue;
|
|
1391
1408
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1409
|
+
addUnit(u);
|
|
1410
|
+
i++;
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
// Start of a word (which may contain the caret). Find its full extent in
|
|
1414
|
+
// the flattened stream so we never wrap mid-word.
|
|
1415
|
+
let j = i;
|
|
1416
|
+
let wordLen = 0; // each unit (including a caret) occupies one column
|
|
1417
|
+
while (j < units.length && !isWs(units[j])) {
|
|
1418
|
+
wordLen += 1;
|
|
1419
|
+
j++;
|
|
1420
|
+
}
|
|
1421
|
+
if (wordLen === 0) {
|
|
1422
|
+
i++;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (lineLen === 0) {
|
|
1426
|
+
if (wordLen > width) {
|
|
1427
|
+
// Overlong word: split into width-sized chunks, keeping the caret in
|
|
1428
|
+
// the chunk that contains it.
|
|
1429
|
+
let from = i;
|
|
1430
|
+
let remaining = wordLen;
|
|
1431
|
+
while (remaining > 0) {
|
|
1432
|
+
const n = Math.min(width, remaining);
|
|
1433
|
+
for (let k = 0; k < n; k++)
|
|
1434
|
+
addUnit(units[from + k]);
|
|
1435
|
+
flushLine();
|
|
1436
|
+
from += n;
|
|
1437
|
+
remaining -= n;
|
|
1438
|
+
}
|
|
1395
1439
|
}
|
|
1396
1440
|
else {
|
|
1397
|
-
|
|
1441
|
+
for (let k = i; k < j; k++)
|
|
1442
|
+
addUnit(units[k]);
|
|
1398
1443
|
}
|
|
1444
|
+
i = j;
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
if (lineLen + wordLen > width) {
|
|
1448
|
+
// Word doesn't fit on the current line; move the whole word to a new
|
|
1449
|
+
// line (leaving any trailing space from the previous word behind).
|
|
1450
|
+
flushLine();
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
for (let k = i; k < j; k++)
|
|
1454
|
+
addUnit(units[k]);
|
|
1455
|
+
i = j;
|
|
1399
1456
|
}
|
|
1400
1457
|
}
|
|
1401
|
-
|
|
1458
|
+
flushLine();
|
|
1459
|
+
if (rows.length === 0)
|
|
1460
|
+
rows.push("");
|
|
1402
1461
|
return { rows, caretRow, caretCol };
|
|
1403
1462
|
}
|
|
1404
1463
|
export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor, hints) {
|
|
@@ -1490,8 +1549,19 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1490
1549
|
}
|
|
1491
1550
|
const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
|
|
1492
1551
|
const labelLen = plainLen(label);
|
|
1493
|
-
|
|
1494
|
-
|
|
1552
|
+
// Truncate the label so the top border never exceeds the box width — a long
|
|
1553
|
+
// label used to push past `boxW` and wrap onto its own row, breaking the box
|
|
1554
|
+
// on narrow/scaled terminals. Reserve room for "╭─── ", the two spaces and
|
|
1555
|
+
// "╮" (7 cols) plus at least one dash.
|
|
1556
|
+
let topLabel = label;
|
|
1557
|
+
let topLabelLen = labelLen;
|
|
1558
|
+
const topRoom = Math.max(0, boxW - 8);
|
|
1559
|
+
if (topLabelLen > topRoom) {
|
|
1560
|
+
topLabel = truncateStyledEllipsis(label, topRoom);
|
|
1561
|
+
topLabelLen = plainLen(topLabel);
|
|
1562
|
+
}
|
|
1563
|
+
const topPad = Math.max(0, boxW - topLabelLen - 7);
|
|
1564
|
+
const top = `\x1b[90m╭─── \x1b[1m\x1b[36m${topLabel}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
|
|
1495
1565
|
const body = [];
|
|
1496
1566
|
for (const l of rows) {
|
|
1497
1567
|
const plain = stripAnsi(l);
|
|
@@ -1503,18 +1573,29 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
|
|
|
1503
1573
|
let totalRows = body.length + 2;
|
|
1504
1574
|
if (hints && (hints.left || hints.right)) {
|
|
1505
1575
|
let left = hints.left ?? "";
|
|
1506
|
-
|
|
1576
|
+
let right = hints.right ?? "";
|
|
1507
1577
|
// Inset the hint row inside the box (box spans columns 0..boxW-1): a
|
|
1508
1578
|
// leading space gives left-edge padding, and the right hint ends one
|
|
1509
|
-
// column before the box's right border for the same padding. Truncate
|
|
1510
|
-
//
|
|
1511
|
-
//
|
|
1579
|
+
// column before the box's right border for the same padding. Truncate so
|
|
1580
|
+
// the row NEVER exceeds the box width on narrow/scaled terminals: the left
|
|
1581
|
+
// hint is cut first to keep the (priority) right hint intact, then the
|
|
1582
|
+
// right hint is cut as a last resort. Otherwise a long right hint would
|
|
1583
|
+
// overflow past the border instead of scaling down.
|
|
1512
1584
|
const hintMax = boxW - 2;
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1585
|
+
const maxText = Math.max(0, hintMax - 1); // reserve a >=1 gap between hints
|
|
1586
|
+
let leftW = plainLen(left);
|
|
1587
|
+
let rightW = plainLen(right);
|
|
1588
|
+
if (leftW + rightW > maxText) {
|
|
1589
|
+
const roomL = Math.max(0, maxText - rightW);
|
|
1590
|
+
left = truncateStyled(left, roomL);
|
|
1591
|
+
leftW = plainLen(left);
|
|
1592
|
+
}
|
|
1593
|
+
const roomR = Math.max(0, maxText - leftW);
|
|
1594
|
+
if (rightW > roomR) {
|
|
1595
|
+
right = truncateStyled(right, roomR);
|
|
1596
|
+
rightW = plainLen(right);
|
|
1516
1597
|
}
|
|
1517
|
-
const gap = Math.max(1,
|
|
1598
|
+
const gap = Math.max(1, hintMax - leftW - rightW);
|
|
1518
1599
|
lines.push(" " + left + " ".repeat(gap) + right);
|
|
1519
1600
|
totalRows += 1;
|
|
1520
1601
|
}
|