@nbtca/prompt 1.5.5 → 1.5.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.
@@ -1,4 +1,4 @@
1
- import { type, space, glyph, brandMark } from '../core/theme.js';
1
+ import { type, space, glyph, brandMark, MAX_FRAME_COLS } from '../core/theme.js';
2
2
  import { pickIcon } from '../core/icons.js';
3
3
  import { t } from '../i18n/index.js';
4
4
  import { clipAnsiToVisualWidth, visualWidth } from '../core/text.js';
@@ -19,7 +19,8 @@ export function resolveChromeLayout(rows) {
19
19
  return { headerLines: 0, footerLines: 0 };
20
20
  }
21
21
  function renderRule(cols) {
22
- const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : 80;
22
+ const terminal = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : 80;
23
+ const width = Math.min(terminal, MAX_FRAME_COLS);
23
24
  const indent = visualWidth(space.indent) < width ? space.indent : '';
24
25
  const ruleWidth = Math.max(1, width - visualWidth(indent) * 2);
25
26
  return indent + type.hint(glyph.rule().repeat(ruleWidth));
@@ -146,8 +147,9 @@ export function renderFooter(_active, cols, tabCount, overrideHint, lineCount =
146
147
  function withPosition(hint, hintText, indent, position, cols) {
147
148
  if (position === undefined)
148
149
  return hint;
149
- const margin = visualWidth(space.indent) < cols ? space.indent : '';
150
- const gap = cols - visualWidth(indent + hintText) - visualWidth(position) - visualWidth(margin);
150
+ const frame = Math.min(cols, MAX_FRAME_COLS);
151
+ const margin = visualWidth(space.indent) < frame ? space.indent : '';
152
+ const gap = frame - visualWidth(indent + hintText) - visualWidth(position) - visualWidth(margin);
151
153
  if (gap < 2)
152
154
  return hint;
153
155
  return hint + ' '.repeat(gap) + type.hint(position) + margin;
@@ -1,14 +1,14 @@
1
1
  import { type, space } from '../../core/theme.js';
2
2
  import { t } from '../../i18n/index.js';
3
3
  import { renderListFieldWithContext } from '../fields/list-field.js';
4
- import { visualWidth, wrapAnsiToVisualWidth, wrapAnsiWithIndent } from '../../core/text.js';
4
+ import { visualWidth, wrapAnsiHanging, wrapAnsiWithIndent } from '../../core/text.js';
5
5
  import { loadingLines } from '../../core/components/spinner.js';
6
6
  function hintLines(label, cols) {
7
7
  return wrapAnsiWithIndent(type.hint(label), cols, space.indent);
8
8
  }
9
9
  function renderReader(lines, cols) {
10
10
  const contentWidth = Math.max(1, Math.min(80, cols - visualWidth(space.indent)));
11
- return lines.flatMap((line) => wrapAnsiToVisualWidth(line, contentWidth).map((part) => `${space.indent}${part}`));
11
+ return lines.flatMap((line) => wrapAnsiHanging(line, contentWidth).map((part) => `${space.indent}${part}`));
12
12
  }
13
13
  function listFieldForState(state) {
14
14
  switch (state.mode) {
package/dist/core/text.js CHANGED
@@ -212,8 +212,37 @@ function renderWrappedSegment(tokens, start, end, activeSgr, activeOsc8) {
212
212
  const osc8AtEnd = advanceOsc8(activeOsc8, tokens, start, end);
213
213
  return osc8AtEnd ? `${withReset}${OSC8_CLOSE}` : withReset;
214
214
  }
215
- export function wrapAnsiToVisualWidth(str, maxWidth) {
215
+ const NO_LINE_START = /^[。.、,;:?!)]}〉》」』】〕・ー~…‥%‰°℃,.;:?!)\]}>]$/u;
216
+ const NO_LINE_END = /^[([{〈《「『【〔([{<]$/u;
217
+ function visibleAt(tokens, index) {
218
+ const token = tokens[index];
219
+ return token && !token.sgr && token.width > 0 ? token : undefined;
220
+ }
221
+ /**
222
+ * Pulls a break earlier so a line never begins with closing punctuation or ends
223
+ * with an opening bracket. Moving the break back keeps every line inside the
224
+ * width, which letting the character hang past the edge would not.
225
+ */
226
+ function applyKinsoku(tokens, start, end) {
227
+ let candidate = end;
228
+ while (candidate > start + 1) {
229
+ const opensNext = visibleAt(tokens, candidate);
230
+ const closesLine = visibleAt(tokens, candidate - 1);
231
+ if (opensNext && NO_LINE_START.test(opensNext.raw)) {
232
+ candidate -= 1;
233
+ continue;
234
+ }
235
+ if (closesLine && NO_LINE_END.test(closesLine.raw)) {
236
+ candidate -= 1;
237
+ continue;
238
+ }
239
+ return candidate;
240
+ }
241
+ return end;
242
+ }
243
+ export function wrapAnsiToVisualWidth(str, maxWidth, continuationWidth = maxWidth) {
216
244
  const widthLimit = Math.max(1, Math.floor(maxWidth));
245
+ const nextLimit = Math.max(1, Math.floor(continuationWidth));
217
246
  if (!Number.isFinite(maxWidth) || visualWidth(str) <= widthLimit)
218
247
  return [str];
219
248
  const tokens = tokenizeForWrapping(str);
@@ -234,7 +263,8 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
234
263
  index += 1;
235
264
  continue;
236
265
  }
237
- if (width + token.width > widthLimit && hasVisible)
266
+ const limit = lines.length === 0 ? widthLimit : nextLimit;
267
+ if (width + token.width > limit && hasVisible)
238
268
  break;
239
269
  width += token.width;
240
270
  hasVisible = true;
@@ -260,6 +290,11 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
260
290
  end = Math.max(index, start + 1);
261
291
  next = end;
262
292
  }
293
+ if (end === next) {
294
+ const adjusted = applyKinsoku(tokens, start, end);
295
+ end = adjusted;
296
+ next = adjusted;
297
+ }
263
298
  lines.push(renderWrappedSegment(tokens, start, end, activeSgr, activeOsc8));
264
299
  activeSgr = advanceSgr(activeSgr, tokens, start, next);
265
300
  activeOsc8 = advanceOsc8(activeOsc8, tokens, start, next);
@@ -267,6 +302,18 @@ export function wrapAnsiToVisualWidth(str, maxWidth) {
267
302
  }
268
303
  return lines.length > 0 ? lines : [''];
269
304
  }
305
+ const LEADING_MARKER = /^(\s*)((?:[-*+•]|\d{1,3}[.)])\s+)?/u;
306
+ export function wrapAnsiHanging(str, maxWidth) {
307
+ const width = Number.isFinite(maxWidth)
308
+ ? Math.max(1, Math.floor(maxWidth))
309
+ : Number.POSITIVE_INFINITY;
310
+ const match = LEADING_MARKER.exec(stripAnsi(str));
311
+ const hang = visualWidth((match?.[1] ?? '') + (match?.[2] ?? ''));
312
+ if (hang === 0 || hang >= width)
313
+ return wrapAnsiToVisualWidth(str, width);
314
+ const continuation = ' '.repeat(hang);
315
+ return wrapAnsiToVisualWidth(str, width, width - hang).map((line, index) => index === 0 ? line : continuation + line);
316
+ }
270
317
  export function wrapAnsiWithIndent(str, maxWidth, preferredIndent = '') {
271
318
  const width = Number.isFinite(maxWidth)
272
319
  ? Math.max(1, Math.floor(maxWidth))
@@ -42,6 +42,8 @@ export const glyph = {
42
42
  barFilled: () => pickIcon('█', '#'),
43
43
  barEmpty: () => pickIcon('░', '-'),
44
44
  };
45
+ /** Beyond this the app stops stretching; a terminal wider than this keeps the slack. */
46
+ export const MAX_FRAME_COLS = 100;
45
47
  export const space = {
46
48
  indent: ' ',
47
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbtca/prompt",
3
- "version": "1.5.5",
3
+ "version": "1.5.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {