@oxecli/oxe 1.0.107 → 1.0.108

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.
Files changed (3) hide show
  1. package/dist/oxe.js +10 -2
  2. package/dist/ui.js +100 -17
  3. package/package.json +1 -1
package/dist/oxe.js CHANGED
@@ -1,10 +1,18 @@
1
1
  import { main } from "./cli.js";
2
+ import { dockTearDown, showCursor } from "./ui.js";
2
3
  export { main };
3
4
  // The TUI keeps the terminal cursor hidden except inside a prompt field. Always
4
- // restore it on exit so the shell isn't left with an invisible cursor.
5
+ // restore it on exit and tear down any docked prompt box so the shell isn't
6
+ // left with a stale prompt box or an invisible cursor.
5
7
  process.on("exit", () => {
6
8
  try {
7
- process.stdout.write("\x1b[?25h");
9
+ dockTearDown();
10
+ }
11
+ catch {
12
+ /* ignore */
13
+ }
14
+ try {
15
+ showCursor();
8
16
  }
9
17
  catch {
10
18
  /* ignore */
package/dist/ui.js CHANGED
@@ -173,6 +173,81 @@ export function truncateStyled(text, maxVisible) {
173
173
  }
174
174
  return out;
175
175
  }
176
+ /**
177
+ * Wrap a styled (ANSI) string into lines of at most `width` visible characters,
178
+ * wrapping whole words onto new lines and splitting any single word longer than
179
+ * `width`. ANSI escape sequences are preserved and re-attached to their glyphs,
180
+ * so no style is lost and no escape sequence is ever cut mid-way.
181
+ */
182
+ function wrapStyledToWidth(line, width) {
183
+ if (width <= 0)
184
+ return [line];
185
+ if (plainLen(line) <= width)
186
+ return [line];
187
+ // Tokenize into glyph units, each carrying the ANSI prefix that precedes it.
188
+ const units = [];
189
+ const re = /(\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07)|([\s\S])/g;
190
+ let pending = "";
191
+ let m;
192
+ re.lastIndex = 0;
193
+ while ((m = re.exec(line)) !== null) {
194
+ if (m[1] !== undefined)
195
+ pending += m[1];
196
+ else {
197
+ units.push({ ch: m[2], pre: pending });
198
+ pending = "";
199
+ }
200
+ }
201
+ const out = [];
202
+ let cur = "";
203
+ let curLen = 0;
204
+ let word = [];
205
+ const emitChunk = (chunk, prependSpace) => {
206
+ const add = (prependSpace && curLen > 0 ? 1 : 0) + chunk.length;
207
+ if (curLen + add > width)
208
+ return false;
209
+ if (prependSpace && curLen > 0) {
210
+ cur += " ";
211
+ curLen++;
212
+ }
213
+ for (const u of chunk)
214
+ cur += u.pre + u.ch;
215
+ curLen += chunk.length;
216
+ return true;
217
+ };
218
+ const flushWord = () => {
219
+ if (!word.length)
220
+ return;
221
+ const wlen = word.length;
222
+ // Split overlong words into <= width chunks so nothing is ever cut off.
223
+ const chunks = [];
224
+ if (wlen <= width)
225
+ chunks.push(word);
226
+ else
227
+ for (let i = 0; i < wlen; i += width)
228
+ chunks.push(word.slice(i, i + width));
229
+ for (let ci = 0; ci < chunks.length; ci++) {
230
+ const chunk = chunks[ci];
231
+ if (!emitChunk(chunk, ci === 0)) {
232
+ out.push(cur);
233
+ cur = "";
234
+ curLen = 0;
235
+ emitChunk(chunk, false);
236
+ }
237
+ }
238
+ word = [];
239
+ };
240
+ for (const u of units) {
241
+ if (/\s/.test(u.ch))
242
+ flushWord();
243
+ else
244
+ word.push(u);
245
+ }
246
+ flushWord();
247
+ if (cur)
248
+ out.push(cur);
249
+ return out.length ? out : [line];
250
+ }
176
251
  // ---------------------------------------------------------------------------
177
252
  // Markup tag -> ANSI
178
253
  // ---------------------------------------------------------------------------
@@ -412,22 +487,11 @@ export function markdownToAnsi(text) {
412
487
  if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
413
488
  line = formatInlineMarkdown(line);
414
489
  }
415
- // Word wrap paragraph line if too long
490
+ // Word wrap paragraph line if too long. Whole words move to a new row; any
491
+ // single word longer than the width is split so nothing is ever cut off at
492
+ // the right edge.
416
493
  if (plainLen(line) > width && !line.startsWith(" ")) {
417
- const words = line.split(" ");
418
- let cur = "";
419
- for (const w of words) {
420
- if (plainLen(cur) + plainLen(w) + 1 > width) {
421
- if (cur)
422
- out.push(cur);
423
- cur = w;
424
- }
425
- else {
426
- cur = cur ? `${cur} ${w}` : w;
427
- }
428
- }
429
- if (cur)
430
- out.push(cur);
494
+ out.push(...wrapStyledToWidth(line, width));
431
495
  }
432
496
  else {
433
497
  out.push(line);
@@ -1377,11 +1441,18 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
1377
1441
  const lines = [top, ...body, bottom];
1378
1442
  let totalRows = body.length + 2;
1379
1443
  if (hints && (hints.left || hints.right)) {
1380
- const left = hints.left ?? "";
1444
+ let left = hints.left ?? "";
1381
1445
  const right = hints.right ?? "";
1382
1446
  // Inset the hint row inside the box (box spans columns 0..boxW-1): a
1383
1447
  // leading space gives left-edge padding, and the right hint ends one
1384
- // column before the box's right border for the same padding.
1448
+ // column before the box's right border for the same padding. Truncate the
1449
+ // left hint so the row never exceeds the box width on narrow terminals
1450
+ // (otherwise it overflows past the right border instead of scaling down).
1451
+ const hintMax = boxW - 2;
1452
+ if (plainLen(left) + plainLen(right) + 1 > hintMax) {
1453
+ const room = Math.max(0, hintMax - plainLen(right) - 1);
1454
+ left = truncateStyled(left, room);
1455
+ }
1385
1456
  const gap = Math.max(1, boxW - 2 - plainLen(left) - plainLen(right));
1386
1457
  lines.push(" " + left + " ".repeat(gap) + right);
1387
1458
  totalRows += 1;
@@ -1504,6 +1575,7 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1504
1575
  dockEnterDocked();
1505
1576
  }
1506
1577
  process.stdin.removeListener("keypress", onKeypress);
1578
+ process.stdout.removeListener("resize", onResize);
1507
1579
  process.stdin.pause();
1508
1580
  showCursor();
1509
1581
  if (isErr)
@@ -1945,6 +2017,17 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1945
2017
  }
1946
2018
  };
1947
2019
  process.stdin.on("keypress", onKeypress);
2020
+ // Reflow the idle prompt box when the terminal resizes so it scales down
2021
+ // instead of wrapping/overflowing its borders on a narrow/scaled terminal.
2022
+ const onResize = () => {
2023
+ try {
2024
+ repaint(false);
2025
+ }
2026
+ catch {
2027
+ /* ignore */
2028
+ }
2029
+ };
2030
+ process.stdout.on("resize", onResize);
1948
2031
  repaint(true);
1949
2032
  });
1950
2033
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.107",
3
+ "version": "1.0.108",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },