@optique/core 1.3.0-dev.2542 → 1.3.0-dev.2558

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.
@@ -17,11 +17,29 @@ function valuesFormatter(theme) {
17
17
  children: [defaultTerminalTheme.values(term, context)]
18
18
  });
19
19
  }
20
+ /** Default callback identity survives any number of per-pass caches. */
21
+ const defaultFormatters = new WeakSet(Object.values(defaultTerminalTheme).filter((value) => typeof value === "function"));
20
22
  function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
23
+ return formatTerminalLeaf(term, theme, quotes, usage).fragment;
24
+ }
25
+ /**
26
+ * Renders a semantic leaf and records the selected callback's provenance.
27
+ * @param term The semantic leaf to render.
28
+ * @param theme The optional role overrides.
29
+ * @param quotes Whether values should use their quoted representation.
30
+ * @param usage The leaf's position within a usage expression.
31
+ * @returns The fragment and whether its callback has default origin.
32
+ * @throws {TypeError} If the term is invalid or is a message-level hard break.
33
+ * @internal
34
+ */
35
+ function formatTerminalLeaf(term, theme = {}, quotes = false, usage) {
21
36
  const quote = (s) => quotes ? `\`${s}\`` : s;
22
37
  let text;
23
38
  switch (term.type) {
24
- case "text": return terminalText(term.text);
39
+ case "text": return {
40
+ fragment: terminalText(term.text),
41
+ defaultOrigin: true
42
+ };
25
43
  case "lineBreak": throw new TypeError("Hard breaks must be rendered at the message level.");
26
44
  case "optionName":
27
45
  text = quote(term.optionName);
@@ -65,19 +83,25 @@ function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
65
83
  usage,
66
84
  format: (child) => formatTerminalTerm(child, theme, quotes, usage)
67
85
  };
86
+ function render(formatter, value) {
87
+ return {
88
+ fragment: formatter(value, ctx),
89
+ defaultOrigin: defaultFormatters.has(formatter)
90
+ };
91
+ }
68
92
  switch (term.type) {
69
- case "optionName": return (theme.optionName ?? defaultTerminalTheme.optionName)(term, ctx);
70
- case "optionNames": return (theme.optionNames ?? defaultTerminalTheme.optionNames)(term, ctx);
71
- case "metavar": return (theme.metavar ?? defaultTerminalTheme.metavar)(term, ctx);
72
- case "value": return (theme.value ?? defaultTerminalTheme.value)(term, ctx);
73
- case "values": return valuesFormatter(theme)(term, ctx);
74
- case "envVar": return (theme.envVar ?? defaultTerminalTheme.envVar)(term, ctx);
75
- case "commandLine": return (theme.commandLine ?? defaultTerminalTheme.commandLine)(term, ctx);
76
- case "url": return (theme.url ?? defaultTerminalTheme.url)(term, ctx);
77
- case "programName": return (theme.programName ?? defaultTerminalTheme.programName)(term, ctx);
78
- case "label": return (theme.label ?? defaultTerminalTheme.label)(term, ctx);
79
- case "syntaxPunctuation": return (theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation)(term, ctx);
80
- case "errorLabel": return (theme.errorLabel ?? defaultTerminalTheme.errorLabel)(term, ctx);
93
+ case "optionName": return render(theme.optionName ?? defaultTerminalTheme.optionName, term);
94
+ case "optionNames": return render(theme.optionNames ?? defaultTerminalTheme.optionNames, term);
95
+ case "metavar": return render(theme.metavar ?? defaultTerminalTheme.metavar, term);
96
+ case "value": return render(theme.value ?? defaultTerminalTheme.value, term);
97
+ case "values": return render(valuesFormatter(theme), term);
98
+ case "envVar": return render(theme.envVar ?? defaultTerminalTheme.envVar, term);
99
+ case "commandLine": return render(theme.commandLine ?? defaultTerminalTheme.commandLine, term);
100
+ case "url": return render(theme.url ?? defaultTerminalTheme.url, term);
101
+ case "programName": return render(theme.programName ?? defaultTerminalTheme.programName, term);
102
+ case "label": return render(theme.label ?? defaultTerminalTheme.label, term);
103
+ case "syntaxPunctuation": return render(theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation, term);
104
+ case "errorLabel": return render(theme.errorLabel ?? defaultTerminalTheme.errorLabel, term);
81
105
  }
82
106
  }
83
107
  const colorNames = [
@@ -225,14 +249,14 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
225
249
  * @since 1.3.0
226
250
  * @internal
227
251
  */
228
- function renderTerminalTerm(term, theme, colors, usage, ambient) {
229
- return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, "", ambient);
252
+ function renderTerminalTerm(term, theme, colors, usage, ambient, resetSuffix = "") {
253
+ return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, resetSuffix, ambient);
230
254
  }
231
255
  /** Memoizes semantic leaves for one layout pass, including measurement. */
232
256
  function cacheTerminalTheme(theme = {}) {
233
257
  function cache(formatter) {
234
258
  const fragments = /* @__PURE__ */ new Map();
235
- return (term, context) => {
259
+ const cached = (term, context) => {
236
260
  const key = JSON.stringify([
237
261
  term,
238
262
  context.quotes,
@@ -245,6 +269,8 @@ function cacheTerminalTheme(theme = {}) {
245
269
  }
246
270
  return fragment;
247
271
  };
272
+ if (defaultFormatters.has(formatter)) defaultFormatters.add(cached);
273
+ return cached;
248
274
  }
249
275
  return {
250
276
  optionName: cache(theme.optionName ?? defaultTerminalTheme.optionName),
@@ -267,4 +293,4 @@ function cacheTerminalTheme(theme = {}) {
267
293
  }
268
294
 
269
295
  //#endregion
270
- export { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
296
+ export { cacheTerminalTheme, formatTerminalLeaf, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
@@ -0,0 +1,87 @@
1
+ const require_displaywidth = require('./displaywidth.cjs');
2
+
3
+ //#region src/text-layout.ts
4
+ /**
5
+ * Measures physical lines after removing terminal escape sequences.
6
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
7
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
8
+ * @returns First, last and widest line widths, and the number of lines.
9
+ * @internal
10
+ */
11
+ function measureText(text) {
12
+ const lines = require_displaywidth.stripAnsi(text).split("\n");
13
+ const widths = lines.map(require_displaywidth.getDisplayWidth);
14
+ return {
15
+ firstLineWidth: widths[0],
16
+ lastLineWidth: widths[widths.length - 1],
17
+ maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
18
+ lineCount: lines.length
19
+ };
20
+ }
21
+ /**
22
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
23
+ * Measures the actual join so combining sequences crossing the boundary are
24
+ * handled correctly. Hard breaks reset both the line and any external offset.
25
+ * This never reflows the text itself or changes its escape sequences.
26
+ * @param text Text to append.
27
+ * @param cursor The current line and column.
28
+ * @param maxWidth Optional column budget.
29
+ * @param trailingRoom Space to leave after a single-line block. An annotation
30
+ * prefix reserves one column for content; a multiline prefix already has a
31
+ * hard break, so its first line needs no reservation.
32
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
33
+ * @internal
34
+ */
35
+ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
36
+ const plain = require_displaywidth.stripAnsi(text);
37
+ const lines = plain.split("\n");
38
+ const offset = cursor.column - require_displaywidth.getDisplayWidth(cursor.line);
39
+ const joinedWidth = offset + require_displaywidth.getDisplayWidth(cursor.line + lines[0]);
40
+ const room = lines.length === 1 ? trailingRoom : 0;
41
+ const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
42
+ const lastLine = lines[lines.length - 1];
43
+ return {
44
+ text: breaks ? "\n" + text : text,
45
+ cursor: lines.length > 1 || breaks ? {
46
+ line: lastLine,
47
+ column: require_displaywidth.getDisplayWidth(lastLine)
48
+ } : {
49
+ line: cursor.line + plain,
50
+ column: joinedWidth
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Measures an annotation's fixed text without invoking theme callbacks.
56
+ * @param prefix The already joined prefix and optional label.
57
+ * @param suffix The closing text.
58
+ * @returns Geometry reserving at least one content column after the suffix's
59
+ * first-line width has been deducted from the formatter budget.
60
+ * @internal
61
+ */
62
+ function measureAnnotation(prefix, suffix) {
63
+ const prefixMetrics = measureText(prefix);
64
+ const suffixMetrics = measureText(suffix);
65
+ return {
66
+ prefix,
67
+ suffix,
68
+ prefixMetrics,
69
+ suffixMetrics,
70
+ minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
71
+ };
72
+ }
73
+ /**
74
+ * Adds a separating space only when a label occupies its final physical line.
75
+ * @param label The rendered label, including any styles and hard breaks.
76
+ * @returns The label with spacing for the text that follows it.
77
+ * @internal
78
+ */
79
+ function spaceAfterLabel(label) {
80
+ return measureText(label).lastLineWidth > 0 ? label + " " : label;
81
+ }
82
+
83
+ //#endregion
84
+ exports.measureAnnotation = measureAnnotation;
85
+ exports.measureText = measureText;
86
+ exports.placeText = placeText;
87
+ exports.spaceAfterLabel = spaceAfterLabel;
@@ -0,0 +1,54 @@
1
+ //#region src/text-layout.d.ts
2
+ /** Physical-line geometry at a rendered-text assembly boundary. @internal */
3
+ interface TextMetrics {
4
+ readonly firstLineWidth: number;
5
+ readonly lastLineWidth: number;
6
+ readonly maxLineWidth: number;
7
+ readonly lineCount: number;
8
+ }
9
+ /**
10
+ * Measures physical lines after removing terminal escape sequences.
11
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
12
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
13
+ * @returns First, last and widest line widths, and the number of lines.
14
+ * @internal
15
+ */
16
+ declare function measureText(text: string): TextMetrics;
17
+ /**
18
+ * The current visible line and its occupied column. The column may include
19
+ * an external offset for text unavailable to this renderer (initialWidth).
20
+ * @internal
21
+ */
22
+ interface TextCursor {
23
+ readonly line: string;
24
+ readonly column: number;
25
+ }
26
+ /**
27
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
28
+ * Measures the actual join so combining sequences crossing the boundary are
29
+ * handled correctly. Hard breaks reset both the line and any external offset.
30
+ * This never reflows the text itself or changes its escape sequences.
31
+ * @param text Text to append.
32
+ * @param cursor The current line and column.
33
+ * @param maxWidth Optional column budget.
34
+ * @param trailingRoom Space to leave after a single-line block. An annotation
35
+ * prefix reserves one column for content; a multiline prefix already has a
36
+ * hard break, so its first line needs no reservation.
37
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
38
+ * @internal
39
+ */
40
+ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
41
+ readonly text: string;
42
+ readonly cursor: TextCursor;
43
+ };
44
+ /** Fixed annotation geometry shared by preflight and placement. @internal */
45
+
46
+ /**
47
+ * Adds a separating space only when a label occupies its final physical line.
48
+ * @param label The rendered label, including any styles and hard breaks.
49
+ * @returns The label with spacing for the text that follows it.
50
+ * @internal
51
+ */
52
+ declare function spaceAfterLabel(label: string): string;
53
+ //#endregion
54
+ export { measureText, placeText, spaceAfterLabel };
@@ -0,0 +1,54 @@
1
+ //#region src/text-layout.d.ts
2
+ /** Physical-line geometry at a rendered-text assembly boundary. @internal */
3
+ interface TextMetrics {
4
+ readonly firstLineWidth: number;
5
+ readonly lastLineWidth: number;
6
+ readonly maxLineWidth: number;
7
+ readonly lineCount: number;
8
+ }
9
+ /**
10
+ * Measures physical lines after removing terminal escape sequences.
11
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
12
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
13
+ * @returns First, last and widest line widths, and the number of lines.
14
+ * @internal
15
+ */
16
+ declare function measureText(text: string): TextMetrics;
17
+ /**
18
+ * The current visible line and its occupied column. The column may include
19
+ * an external offset for text unavailable to this renderer (initialWidth).
20
+ * @internal
21
+ */
22
+ interface TextCursor {
23
+ readonly line: string;
24
+ readonly column: number;
25
+ }
26
+ /**
27
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
28
+ * Measures the actual join so combining sequences crossing the boundary are
29
+ * handled correctly. Hard breaks reset both the line and any external offset.
30
+ * This never reflows the text itself or changes its escape sequences.
31
+ * @param text Text to append.
32
+ * @param cursor The current line and column.
33
+ * @param maxWidth Optional column budget.
34
+ * @param trailingRoom Space to leave after a single-line block. An annotation
35
+ * prefix reserves one column for content; a multiline prefix already has a
36
+ * hard break, so its first line needs no reservation.
37
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
38
+ * @internal
39
+ */
40
+ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
41
+ readonly text: string;
42
+ readonly cursor: TextCursor;
43
+ };
44
+ /** Fixed annotation geometry shared by preflight and placement. @internal */
45
+
46
+ /**
47
+ * Adds a separating space only when a label occupies its final physical line.
48
+ * @param label The rendered label, including any styles and hard breaks.
49
+ * @returns The label with spacing for the text that follows it.
50
+ * @internal
51
+ */
52
+ declare function spaceAfterLabel(label: string): string;
53
+ //#endregion
54
+ export { measureText, placeText, spaceAfterLabel };
@@ -0,0 +1,84 @@
1
+ import { getDisplayWidth, stripAnsi } from "./displaywidth.js";
2
+
3
+ //#region src/text-layout.ts
4
+ /**
5
+ * Measures physical lines after removing terminal escape sequences.
6
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
7
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
8
+ * @returns First, last and widest line widths, and the number of lines.
9
+ * @internal
10
+ */
11
+ function measureText(text) {
12
+ const lines = stripAnsi(text).split("\n");
13
+ const widths = lines.map(getDisplayWidth);
14
+ return {
15
+ firstLineWidth: widths[0],
16
+ lastLineWidth: widths[widths.length - 1],
17
+ maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
18
+ lineCount: lines.length
19
+ };
20
+ }
21
+ /**
22
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
23
+ * Measures the actual join so combining sequences crossing the boundary are
24
+ * handled correctly. Hard breaks reset both the line and any external offset.
25
+ * This never reflows the text itself or changes its escape sequences.
26
+ * @param text Text to append.
27
+ * @param cursor The current line and column.
28
+ * @param maxWidth Optional column budget.
29
+ * @param trailingRoom Space to leave after a single-line block. An annotation
30
+ * prefix reserves one column for content; a multiline prefix already has a
31
+ * hard break, so its first line needs no reservation.
32
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
33
+ * @internal
34
+ */
35
+ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
36
+ const plain = stripAnsi(text);
37
+ const lines = plain.split("\n");
38
+ const offset = cursor.column - getDisplayWidth(cursor.line);
39
+ const joinedWidth = offset + getDisplayWidth(cursor.line + lines[0]);
40
+ const room = lines.length === 1 ? trailingRoom : 0;
41
+ const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
42
+ const lastLine = lines[lines.length - 1];
43
+ return {
44
+ text: breaks ? "\n" + text : text,
45
+ cursor: lines.length > 1 || breaks ? {
46
+ line: lastLine,
47
+ column: getDisplayWidth(lastLine)
48
+ } : {
49
+ line: cursor.line + plain,
50
+ column: joinedWidth
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Measures an annotation's fixed text without invoking theme callbacks.
56
+ * @param prefix The already joined prefix and optional label.
57
+ * @param suffix The closing text.
58
+ * @returns Geometry reserving at least one content column after the suffix's
59
+ * first-line width has been deducted from the formatter budget.
60
+ * @internal
61
+ */
62
+ function measureAnnotation(prefix, suffix) {
63
+ const prefixMetrics = measureText(prefix);
64
+ const suffixMetrics = measureText(suffix);
65
+ return {
66
+ prefix,
67
+ suffix,
68
+ prefixMetrics,
69
+ suffixMetrics,
70
+ minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
71
+ };
72
+ }
73
+ /**
74
+ * Adds a separating space only when a label occupies its final physical line.
75
+ * @param label The rendered label, including any styles and hard breaks.
76
+ * @returns The label with spacing for the text that follows it.
77
+ * @internal
78
+ */
79
+ function spaceAfterLabel(label) {
80
+ return measureText(label).lastLineWidth > 0 ? label + " " : label;
81
+ }
82
+
83
+ //#endregion
84
+ export { measureAnnotation, measureText, placeText, spaceAfterLabel };
package/dist/usage.cjs CHANGED
@@ -204,23 +204,17 @@ function formatUsage(programName, usage, options = {}) {
204
204
  if (lines.length > 0) return lines.join("\n");
205
205
  }
206
206
  }
207
- const programTokens = layoutUsageTokens(require_terminal_internal.fragmentTokens(require_terminal_internal.formatTerminalTerm({
208
- type: "programName",
209
- programName
210
- }, options.theme)), options);
211
- const terms = [...formatUsageTerms(usage, options)];
212
- if (terms.length === 0) return require_terminal_internal.serializeTokens(programTokens, options.colors);
213
- const width = programTokens.reduce((n, token) => token.width === -1 ? 0 : n + token.width, 0);
214
- const separator = options.maxWidth != null && width + 1 + terms[0].width > options.maxWidth ? {
215
- text: "\n",
216
- width: -1,
217
- scopes: []
218
- } : {
219
- text: " ",
220
- width: 1,
221
- scopes: []
222
- };
223
- return require_terminal_internal.serializeTokens([...programTokens, separator], options.colors) + wrapUsageTokens(terms, options, separator.width === -1 ? 0 : width + 1);
207
+ return wrapUsageTokens(function* () {
208
+ yield* usageLeafTokens({
209
+ type: "programName",
210
+ programName
211
+ }, options);
212
+ yield {
213
+ ...usageSpace,
214
+ programBoundary: true
215
+ };
216
+ yield* formatUsageTerms(usage, options);
217
+ }(), options);
224
218
  }
225
219
  /**
226
220
  * Normalizes a usage description by flattening nested exclusive terms,
@@ -450,14 +444,23 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
450
444
  }
451
445
  return terms;
452
446
  }
447
+ const usageSpace = {
448
+ text: " ",
449
+ width: 1,
450
+ scopes: [],
451
+ separator: true
452
+ };
453
+ function* usageLeafTokens(term, options, usage) {
454
+ const { fragment, defaultOrigin } = require_terminal_internal.formatTerminalLeaf(term, options.theme, false, usage);
455
+ for (const token of require_terminal_internal.fragmentTokens(fragment)) yield {
456
+ ...token,
457
+ legacyWhitespace: defaultOrigin
458
+ };
459
+ }
453
460
  function* formatUsageTerms(terms, options) {
454
461
  let i = 0;
455
462
  for (const t of terms) {
456
- if (i > 0) yield {
457
- text: " ",
458
- width: 1,
459
- scopes: []
460
- };
463
+ if (i > 0) yield usageSpace;
461
464
  yield* formatUsageTermInternal(t, options);
462
465
  i++;
463
466
  }
@@ -483,21 +486,57 @@ function formatUsageTerm(term, options = {}) {
483
486
  if (visibleTerms.length < 1) return "";
484
487
  return wrapUsageTokens(formatUsageTermInternal(visibleTerms[0], options), options);
485
488
  }
486
- function wrapUsageTokens(input, options, initialWidth = 0) {
487
- return require_terminal_internal.serializeTokens(layoutUsageTokens(input, options, initialWidth), options.colors);
489
+ function wrapUsageTokens(input, options) {
490
+ return require_terminal_internal.serializeTokens(layoutUsageTokens(input, options), options.colors);
488
491
  }
489
- function layoutUsageTokens(input, options, initialWidth = 0) {
490
- let lineWidth = initialWidth;
491
- const output = [];
492
+ /** Keeps one layout-owned space between nonempty outputs on the same line. */
493
+ function* normalizeUsageSeparators(input) {
494
+ let previous;
495
+ let pending = [];
492
496
  for (const token of input) {
497
+ if (token.separator || token.text === "") {
498
+ pending.push(token);
499
+ continue;
500
+ }
501
+ let separated = false;
502
+ for (const item of pending) if (!item.separator) yield item;
503
+ else if (!separated && previous != null && previous.width !== -1 && token.width !== -1) {
504
+ yield item;
505
+ separated = true;
506
+ }
507
+ pending = [];
508
+ yield token;
509
+ previous = token;
510
+ }
511
+ for (const item of pending) if (!item.separator) yield item;
512
+ }
513
+ function layoutUsageTokens(input, options) {
514
+ let lineWidth = 0;
515
+ const output = [];
516
+ const tokens = [...normalizeUsageSeparators(input)];
517
+ for (const [index, token] of tokens.entries()) {
518
+ if (token.programBoundary && options.maxWidth != null) {
519
+ const next = tokens.find((item, i) => i > index && item.text !== "");
520
+ if (next != null && lineWidth + token.width + next.width > options.maxWidth) {
521
+ output.push({
522
+ text: "\n",
523
+ width: -1,
524
+ scopes: []
525
+ });
526
+ lineWidth = 0;
527
+ continue;
528
+ }
529
+ }
493
530
  if (token.width === -1) {
494
531
  output.push(token);
495
532
  lineWidth = 0;
496
533
  continue;
497
534
  }
498
- if (options.maxWidth != null && lineWidth > 0 && lineWidth + token.width > options.maxWidth) {
499
- const last = output.at(-1);
500
- if (last?.text.endsWith(" ") && (!options.colors || last.scopes.length === 0)) output[output.length - 1] = {
535
+ if (options.maxWidth != null && lineWidth > 0 && token.width > 0 && lineWidth + token.width > options.maxWidth) {
536
+ let lastIndex = output.length - 1;
537
+ while (lastIndex >= 0 && output[lastIndex].text === "") lastIndex--;
538
+ const last = output[lastIndex];
539
+ if (last?.text.endsWith(" ") && (last.separator || last.legacyWhitespace && (!options.colors || last.scopes.length === 0))) output[lastIndex] = {
501
540
  ...last,
502
541
  text: last.text.slice(0, -1),
503
542
  width: last.width - 1
@@ -508,7 +547,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
508
547
  scopes: []
509
548
  });
510
549
  lineWidth = 0;
511
- if (token.text === " ") continue;
550
+ if (token.separator || token.legacyWhitespace && token.text === " ") continue;
512
551
  }
513
552
  output.push(token);
514
553
  lineWidth += token.width;
@@ -517,7 +556,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
517
556
  }
518
557
  function* formatUsageTermInternal(term, options) {
519
558
  function leaf(t, usage) {
520
- return require_terminal_internal.fragmentTokens(require_terminal_internal.formatTerminalTerm(t, options.theme, false, usage));
559
+ return usageLeafTokens(t, options, usage);
521
560
  }
522
561
  function punctuation(punctuation$1, kind) {
523
562
  return leaf({
@@ -526,11 +565,7 @@ function* formatUsageTermInternal(term, options) {
526
565
  kind
527
566
  });
528
567
  }
529
- const space = {
530
- text: " ",
531
- width: 1,
532
- scopes: []
533
- };
568
+ const space = usageSpace;
534
569
  if (term.type === "argument") yield* leaf({
535
570
  type: "metavar",
536
571
  metavar: term.metavar