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

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,6 +1,15 @@
1
1
 
2
2
  //#region src/displaywidth.ts
3
3
  const ansiRegex = /\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07))/g;
4
+ /**
5
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
6
+ * @param text Rendered terminal text.
7
+ * @returns Text without terminal escape sequences.
8
+ * @internal
9
+ */
10
+ function stripAnsi(text) {
11
+ return text.replace(ansiRegex, "");
12
+ }
4
13
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
5
14
  /**
6
15
  * Computes the terminal display width of a string, accounting for
@@ -11,7 +20,7 @@ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
11
20
  * @internal
12
21
  */
13
22
  function getDisplayWidth(text) {
14
- const stripped = text.replace(ansiRegex, "");
23
+ const stripped = stripAnsi(text);
15
24
  let width = 0;
16
25
  for (const { segment } of segmenter.segment(stripped)) width += graphemeWidth(segment);
17
26
  return width;
@@ -41,4 +50,5 @@ function isEastAsianWide(cp) {
41
50
  }
42
51
 
43
52
  //#endregion
44
- exports.getDisplayWidth = getDisplayWidth;
53
+ exports.getDisplayWidth = getDisplayWidth;
54
+ exports.stripAnsi = stripAnsi;
@@ -1,4 +1,11 @@
1
1
  //#region src/displaywidth.d.ts
2
+ /**
3
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
4
+ * @param text Rendered terminal text.
5
+ * @returns Text without terminal escape sequences.
6
+ * @internal
7
+ */
8
+ declare function stripAnsi(text: string): string;
2
9
  /**
3
10
  * Computes the terminal display width of a string, accounting for
4
11
  * East Asian wide characters, combining marks, emoji, and ANSI escapes.
@@ -9,4 +16,4 @@
9
16
  */
10
17
  declare function getDisplayWidth(text: string): number;
11
18
  //#endregion
12
- export { getDisplayWidth };
19
+ export { getDisplayWidth, stripAnsi };
@@ -1,4 +1,11 @@
1
1
  //#region src/displaywidth.d.ts
2
+ /**
3
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
4
+ * @param text Rendered terminal text.
5
+ * @returns Text without terminal escape sequences.
6
+ * @internal
7
+ */
8
+ declare function stripAnsi(text: string): string;
2
9
  /**
3
10
  * Computes the terminal display width of a string, accounting for
4
11
  * East Asian wide characters, combining marks, emoji, and ANSI escapes.
@@ -9,4 +16,4 @@
9
16
  */
10
17
  declare function getDisplayWidth(text: string): number;
11
18
  //#endregion
12
- export { getDisplayWidth };
19
+ export { getDisplayWidth, stripAnsi };
@@ -1,5 +1,14 @@
1
1
  //#region src/displaywidth.ts
2
2
  const ansiRegex = /\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07))/g;
3
+ /**
4
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
5
+ * @param text Rendered terminal text.
6
+ * @returns Text without terminal escape sequences.
7
+ * @internal
8
+ */
9
+ function stripAnsi(text) {
10
+ return text.replace(ansiRegex, "");
11
+ }
3
12
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
4
13
  /**
5
14
  * Computes the terminal display width of a string, accounting for
@@ -10,7 +19,7 @@ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
10
19
  * @internal
11
20
  */
12
21
  function getDisplayWidth(text) {
13
- const stripped = text.replace(ansiRegex, "");
22
+ const stripped = stripAnsi(text);
14
23
  let width = 0;
15
24
  for (const { segment } of segmenter.segment(stripped)) width += graphemeWidth(segment);
16
25
  return width;
@@ -40,4 +49,4 @@ function isEastAsianWide(cp) {
40
49
  }
41
50
 
42
51
  //#endregion
43
- export { getDisplayWidth };
52
+ export { getDisplayWidth, stripAnsi };
package/dist/doc.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_message_registry = require('./message-registry.cjs');
2
- const require_displaywidth = require('./displaywidth.cjs');
3
2
  const require_terminal_internal = require('./terminal-internal.cjs');
4
3
  const require_message = require('./message.cjs');
5
4
  const require_message_renderer = require('./message-renderer.cjs');
5
+ const require_text_layout = require('./text-layout.cjs');
6
6
  const require_validate = require('./validate.cjs');
7
7
  const require_usage = require('./usage.cjs');
8
8
 
@@ -251,8 +251,8 @@ function formatDocPage(programName, page, options = {}) {
251
251
  }
252
252
  return rendered;
253
253
  };
254
- const usageLabel = page.usage != null && options.showUsage !== false ? label("Usage:", "usage") + " " : "";
255
- const usageLabelWidth = require_displaywidth.getDisplayWidth(usageLabel.split("\n").at(-1) ?? "");
254
+ const usageLabel = page.usage != null && options.showUsage !== false ? require_text_layout.spaceAfterLabel(label("Usage:", "usage")) : "";
255
+ const usageLabelWidth = require_text_layout.measureText(usageLabel).lastLineWidth;
256
256
  const defaultStyle = require_terminal_internal.styleCode(options.theme?.annotationStyles?.default ?? { dim: true });
257
257
  const choicesStyle = require_terminal_internal.styleCode(options.theme?.annotationStyles?.choices ?? { dim: true });
258
258
  require_validate.validateProgramName(programName);
@@ -276,6 +276,18 @@ function formatDocPage(programName, page, options = {}) {
276
276
  }
277
277
  const hasContent = (msg) => Array.isArray(msg) && msg.length > 0;
278
278
  const needsDescriptionColumn = (entry) => hasContent(entry.description) || (options.showDefault === true || typeof options.showDefault === "object") && hasContent(entry.default) || (options.showChoices === true || typeof options.showChoices === "object") && hasContent(entry.choices);
279
+ const annotations = /* @__PURE__ */ new Map();
280
+ const annotation = (kind) => {
281
+ const cached = annotations.get(kind);
282
+ if (cached != null) return cached;
283
+ const config = kind === "default" ? options.showDefault : options.showChoices;
284
+ const prefix = punctuation(typeof config === "object" ? config.prefix ?? (kind === "default" ? " [" : " (") : kind === "default" ? " [" : " (", kind === "default" ? "defaultPrefix" : "choicesPrefix");
285
+ const suffix = punctuation(typeof config === "object" ? config.suffix ?? (kind === "default" ? "]" : ")") : kind === "default" ? "]" : ")", kind === "default" ? "defaultSuffix" : "choicesSuffix");
286
+ const choicesLabel = kind === "choices" ? label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices") : "";
287
+ const layout = require_text_layout.measureAnnotation(prefix + choicesLabel, suffix);
288
+ annotations.set(kind, layout);
289
+ return layout;
290
+ };
279
291
  const automaticTermWidth = () => {
280
292
  let widest;
281
293
  for (const section of page.sections) for (const entry of section.entries) {
@@ -286,7 +298,7 @@ function formatDocPage(programName, page, options = {}) {
286
298
  optionsSeparator: ", ",
287
299
  context: "doc"
288
300
  });
289
- const width = Math.max(...rendered.split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
301
+ const width = require_text_layout.measureText(rendered).maxLineWidth;
290
302
  widest = widest == null ? width : Math.max(widest, width);
291
303
  }
292
304
  return widest;
@@ -303,16 +315,9 @@ function formatDocPage(programName, page, options = {}) {
303
315
  }
304
316
  if (options.maxWidth != null) {
305
317
  let minDescWidth = 1;
306
- if (needsDescColumn) {
307
- if (options.showDefault && page.sections.some((s) => s.entries.some((e) => hasContent(e.default)))) {
308
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
309
- minDescWidth = Math.max(minDescWidth, require_displaywidth.getDisplayWidth(prefix));
310
- }
311
- if (options.showChoices && page.sections.some((s) => s.entries.some((e) => hasContent(e.choices)))) {
312
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
313
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
314
- minDescWidth = Math.max(minDescWidth, require_displaywidth.getDisplayWidth(prefix) + require_displaywidth.getDisplayWidth(choicesLabelText));
315
- }
318
+ if (needsDescColumn) for (const kind of ["default", "choices"]) {
319
+ const enabled = kind === "default" ? options.showDefault : options.showChoices;
320
+ if (enabled && page.sections.some((section) => section.entries.some((entry) => hasContent(entry[kind])))) minDescWidth = Math.max(minDescWidth, annotation(kind).minWidth);
316
321
  }
317
322
  const splitEntryMin = termIndent + 2 + Math.max(2, 2 * minDescWidth - 1);
318
323
  const fixedEntryMin = termIndent + 2 + termWidth + minDescWidth;
@@ -321,11 +326,17 @@ function formatDocPage(programName, page, options = {}) {
321
326
  type: "programName",
322
327
  programName
323
328
  }, options.theme))].map((token) => token.width)) : 0;
324
- const usageMin = page.usage != null && showUsage ? usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth)) : 1;
329
+ const usageMin = page.usage != null && showUsage ? Math.max(require_text_layout.measureText(usageLabel).maxLineWidth, usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth))) : 1;
325
330
  let sectionMin = 1;
326
- if (hasContent(page.examples)) sectionMin = Math.max(sectionMin, ...label("Examples:", "examples").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
327
- if (hasContent(page.author)) sectionMin = Math.max(sectionMin, ...label("Author:", "author").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
328
- if (hasContent(page.bugs)) sectionMin = Math.max(sectionMin, ...label("Bugs:", "bugs").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
331
+ for (const kind of [
332
+ "examples",
333
+ "author",
334
+ "bugs"
335
+ ]) {
336
+ if (!hasContent(page[kind])) continue;
337
+ const title = kind === "examples" ? "Examples:" : kind === "author" ? "Author:" : "Bugs:";
338
+ sectionMin = Math.max(sectionMin, 3, require_text_layout.measureText(label(title, kind)).maxLineWidth);
339
+ }
329
340
  const minWidth = Math.max(entryMin, usageMin, sectionMin);
330
341
  if (options.maxWidth < minWidth) throw new RangeError(`maxWidth must be at least ${minWidth}, got ${options.maxWidth}.`);
331
342
  if (needsDescColumn && minDescWidth > 1) {
@@ -383,17 +394,21 @@ function formatDocPage(programName, page, options = {}) {
383
394
  output += sectionLabel;
384
395
  }
385
396
  for (const entry of section.entries) {
386
- const term = require_usage.formatUsageTerm(entry.term, {
397
+ const termOptions = {
387
398
  colors: options.colors,
388
399
  theme: options.theme,
389
400
  optionsSeparator: ", ",
390
- context: "doc",
391
- maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
401
+ context: "doc"
402
+ };
403
+ const unwrappedTerm = require_usage.formatUsageTerm(entry.term, termOptions);
404
+ const multilineReplacement = require_text_layout.measureText(unwrappedTerm).lineCount > 1;
405
+ const term = options.maxWidth == null ? unwrappedTerm : require_usage.formatUsageTerm(entry.term, {
406
+ ...termOptions,
407
+ maxWidth: options.maxWidth - termIndent
392
408
  });
393
409
  const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - effectiveTermWidth - 2;
394
- const termVisibleWidth = lastLineVisibleLength(term);
410
+ const termVisibleWidth = require_text_layout.measureText(term).lastLineWidth;
395
411
  const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - effectiveTermWidth) : 0;
396
- const currentExtraOffset = () => description.includes("\n") ? 0 : extraTermOffset;
397
412
  const descFormatOptions = {
398
413
  colors: options.colors,
399
414
  quotes: !options.colors,
@@ -401,35 +416,26 @@ function formatDocPage(programName, page, options = {}) {
401
416
  initialWidth: extraTermOffset > 0 ? extraTermOffset : void 0
402
417
  };
403
418
  let description = entry.description == null ? "" : formatMessage(entry.description, descFormatOptions);
404
- if (options.showDefault && hasContent(entry.default)) {
405
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
406
- const suffix = punctuation(typeof options.showDefault === "object" ? options.showDefault.suffix ?? "]" : "]", "defaultSuffix");
407
- const prefixWidth = require_displaywidth.getDisplayWidth(prefix);
408
- const suffixWidth = require_displaywidth.getDisplayWidth(suffix);
409
- let defaultStartWidth;
410
- if (descColumnWidth != null) {
411
- const lastW = lastLineVisibleLength(description);
412
- const effectiveLastW = lastW + currentExtraOffset();
413
- if (effectiveLastW + prefixWidth >= descColumnWidth) {
414
- description += "\n";
415
- defaultStartWidth = prefixWidth;
416
- } else defaultStartWidth = effectiveLastW + prefixWidth;
417
- }
418
- const defaultFormatOptions = {
419
- colors: options.colors ? { resetSuffix: defaultStyle } : false,
420
- quotes: !options.colors,
421
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffixWidth,
422
- initialWidth: defaultStartWidth
423
- };
424
- const defaultContent = formatMessage(entry.default, defaultFormatOptions, options.theme?.annotationStyles?.default ?? { dim: true });
425
- const defaultText = `${prefix}${defaultContent}${opaqueFormatter && options.colors ? defaultStyle : ""}${suffix}`;
426
- const formattedDefault = options.colors ? `${defaultStyle}${defaultText}${defaultStyle ? "\x1B[0m" : ""}` : defaultText;
427
- description += formattedDefault;
428
- }
419
+ let cursor = require_text_layout.placeText(description, {
420
+ line: "",
421
+ column: extraTermOffset
422
+ }).cursor;
423
+ const appendAnnotation = (content, layout, style, ambient, quotes) => {
424
+ const prefix = require_text_layout.placeText(layout.prefix, cursor, descColumnWidth, 1);
425
+ if (prefix.text !== layout.prefix) description += "\n";
426
+ const rendered = formatMessage(content, {
427
+ colors: options.colors ? { resetSuffix: style } : false,
428
+ quotes,
429
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - layout.suffixMetrics.firstLineWidth,
430
+ initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
431
+ }, ambient);
432
+ const suffix = require_text_layout.placeText(layout.suffix, require_text_layout.placeText(rendered, prefix.cursor).cursor, descColumnWidth);
433
+ const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
434
+ description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
435
+ cursor = suffix.cursor;
436
+ };
437
+ if (options.showDefault && hasContent(entry.default)) appendAnnotation(entry.default, annotation("default"), defaultStyle, defaultAmbient, !options.colors);
429
438
  if (options.showChoices && hasContent(entry.choices)) {
430
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
431
- const suffix = punctuation(typeof options.showChoices === "object" ? options.showChoices.suffix ?? ")" : ")", "choicesSuffix");
432
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
433
439
  const maxItems = typeof options.showChoices === "object" ? options.showChoices.maxItems ?? 8 : 8;
434
440
  const terms = Array.isArray(entry.choices) ? entry.choices : [];
435
441
  let truncatedTerms = terms;
@@ -447,31 +453,10 @@ function formatDocPage(programName, page, options = {}) {
447
453
  }
448
454
  if (truncated) truncatedTerms = [...terms.slice(0, cutIndex), require_message.text(", ...")];
449
455
  }
450
- const choicesPrefixWidth = require_displaywidth.getDisplayWidth(prefix);
451
- const choicesSuffixWidth = require_displaywidth.getDisplayWidth(suffix);
452
- const choicesLabelWidth = require_displaywidth.getDisplayWidth(choicesLabelText);
453
- let choicesStartWidth;
454
- if (descColumnWidth != null) {
455
- const lastW = lastLineVisibleLength(description);
456
- const effectiveLastW = lastW + currentExtraOffset();
457
- const prefixLabelLen = choicesPrefixWidth + choicesLabelWidth;
458
- if (effectiveLastW + prefixLabelLen >= descColumnWidth) {
459
- description += "\n";
460
- choicesStartWidth = prefixLabelLen;
461
- } else choicesStartWidth = effectiveLastW + prefixLabelLen;
462
- }
463
- const choicesFormatOptions = {
464
- colors: options.colors ? { resetSuffix: choicesStyle } : false,
465
- quotes: false,
466
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - choicesSuffixWidth,
467
- initialWidth: choicesStartWidth
468
- };
469
- const choicesDisplay = formatMessage(truncatedTerms, choicesFormatOptions, options.theme?.annotationStyles?.choices ?? { dim: true });
470
- const choicesText = `${prefix}${choicesLabelText}${choicesDisplay}${opaqueFormatter && options.colors ? choicesStyle : ""}${suffix}`;
471
- const formattedChoices = options.colors ? `${choicesStyle}${choicesText}${choicesStyle ? "\x1B[0m" : ""}` : choicesText;
472
- description += formattedChoices;
456
+ appendAnnotation(truncatedTerms, annotation("choices"), choicesStyle, choicesAmbient, false);
473
457
  }
474
- output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, effectiveTermWidth)}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
458
+ const paddedTerm = ansiAwareRightPad(term, effectiveTermWidth);
459
+ output += `${" ".repeat(termIndent)}${multilineReplacement ? indentLines(paddedTerm, termIndent) : paddedTerm}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
475
460
  }
476
461
  }
477
462
  if (hasContent(page.examples)) {
@@ -530,21 +515,16 @@ function indentLines(text$1, indent) {
530
515
  * contribute to the rendered width.
531
516
  */
532
517
  function maxVisibleAtomicWidth(usage, theme) {
533
- return Math.max(0, ...usage.flatMap((term) => require_usage.formatUsageTerm(term, {
518
+ return usage.reduce((widest, term) => Math.max(widest, require_text_layout.measureText(require_usage.formatUsageTerm(term, {
534
519
  theme,
535
520
  maxWidth: 1
536
- }).split("\n").map(require_displaywidth.getDisplayWidth)));
521
+ })).maxLineWidth), 0);
537
522
  }
538
523
  function ansiAwareRightPad(text$1, length, char = " ") {
539
- const visibleWidth = lastLineVisibleLength(text$1);
524
+ const visibleWidth = require_text_layout.measureText(text$1).lastLineWidth;
540
525
  if (visibleWidth >= length) return text$1;
541
526
  return text$1 + char.repeat(length - visibleWidth);
542
527
  }
543
- function lastLineVisibleLength(text$1) {
544
- const lastNewline = text$1.lastIndexOf("\n");
545
- const lastLine = lastNewline === -1 ? text$1 : text$1.slice(lastNewline + 1);
546
- return require_displaywidth.getDisplayWidth(lastLine);
547
- }
548
528
 
549
529
  //#endregion
550
530
  exports.cloneDocEntry = cloneDocEntry;
package/dist/doc.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { messageRenderers } from "./message-registry.js";
2
- import { getDisplayWidth } from "./displaywidth.js";
3
2
  import { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, styleCode } from "./terminal-internal.js";
4
3
  import { cloneMessage, text } from "./message.js";
5
4
  import { resolveMessageFormatter } from "./message-renderer.js";
5
+ import { measureAnnotation, measureText, placeText, spaceAfterLabel } from "./text-layout.js";
6
6
  import { validateLabel, validateProgramName } from "./validate.js";
7
7
  import { cloneUsageTerm, formatUsage, formatUsageTerm, isDocHidden } from "./usage.js";
8
8
 
@@ -251,8 +251,8 @@ function formatDocPage(programName, page, options = {}) {
251
251
  }
252
252
  return rendered;
253
253
  };
254
- const usageLabel = page.usage != null && options.showUsage !== false ? label("Usage:", "usage") + " " : "";
255
- const usageLabelWidth = getDisplayWidth(usageLabel.split("\n").at(-1) ?? "");
254
+ const usageLabel = page.usage != null && options.showUsage !== false ? spaceAfterLabel(label("Usage:", "usage")) : "";
255
+ const usageLabelWidth = measureText(usageLabel).lastLineWidth;
256
256
  const defaultStyle = styleCode(options.theme?.annotationStyles?.default ?? { dim: true });
257
257
  const choicesStyle = styleCode(options.theme?.annotationStyles?.choices ?? { dim: true });
258
258
  validateProgramName(programName);
@@ -276,6 +276,18 @@ function formatDocPage(programName, page, options = {}) {
276
276
  }
277
277
  const hasContent = (msg) => Array.isArray(msg) && msg.length > 0;
278
278
  const needsDescriptionColumn = (entry) => hasContent(entry.description) || (options.showDefault === true || typeof options.showDefault === "object") && hasContent(entry.default) || (options.showChoices === true || typeof options.showChoices === "object") && hasContent(entry.choices);
279
+ const annotations = /* @__PURE__ */ new Map();
280
+ const annotation = (kind) => {
281
+ const cached = annotations.get(kind);
282
+ if (cached != null) return cached;
283
+ const config = kind === "default" ? options.showDefault : options.showChoices;
284
+ const prefix = punctuation(typeof config === "object" ? config.prefix ?? (kind === "default" ? " [" : " (") : kind === "default" ? " [" : " (", kind === "default" ? "defaultPrefix" : "choicesPrefix");
285
+ const suffix = punctuation(typeof config === "object" ? config.suffix ?? (kind === "default" ? "]" : ")") : kind === "default" ? "]" : ")", kind === "default" ? "defaultSuffix" : "choicesSuffix");
286
+ const choicesLabel = kind === "choices" ? label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices") : "";
287
+ const layout = measureAnnotation(prefix + choicesLabel, suffix);
288
+ annotations.set(kind, layout);
289
+ return layout;
290
+ };
279
291
  const automaticTermWidth = () => {
280
292
  let widest;
281
293
  for (const section of page.sections) for (const entry of section.entries) {
@@ -286,7 +298,7 @@ function formatDocPage(programName, page, options = {}) {
286
298
  optionsSeparator: ", ",
287
299
  context: "doc"
288
300
  });
289
- const width = Math.max(...rendered.split("\n").map((line) => getDisplayWidth(line)));
301
+ const width = measureText(rendered).maxLineWidth;
290
302
  widest = widest == null ? width : Math.max(widest, width);
291
303
  }
292
304
  return widest;
@@ -303,16 +315,9 @@ function formatDocPage(programName, page, options = {}) {
303
315
  }
304
316
  if (options.maxWidth != null) {
305
317
  let minDescWidth = 1;
306
- if (needsDescColumn) {
307
- if (options.showDefault && page.sections.some((s) => s.entries.some((e) => hasContent(e.default)))) {
308
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
309
- minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix));
310
- }
311
- if (options.showChoices && page.sections.some((s) => s.entries.some((e) => hasContent(e.choices)))) {
312
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
313
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
314
- minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix) + getDisplayWidth(choicesLabelText));
315
- }
318
+ if (needsDescColumn) for (const kind of ["default", "choices"]) {
319
+ const enabled = kind === "default" ? options.showDefault : options.showChoices;
320
+ if (enabled && page.sections.some((section) => section.entries.some((entry) => hasContent(entry[kind])))) minDescWidth = Math.max(minDescWidth, annotation(kind).minWidth);
316
321
  }
317
322
  const splitEntryMin = termIndent + 2 + Math.max(2, 2 * minDescWidth - 1);
318
323
  const fixedEntryMin = termIndent + 2 + termWidth + minDescWidth;
@@ -321,11 +326,17 @@ function formatDocPage(programName, page, options = {}) {
321
326
  type: "programName",
322
327
  programName
323
328
  }, options.theme))].map((token) => token.width)) : 0;
324
- const usageMin = page.usage != null && showUsage ? usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth)) : 1;
329
+ const usageMin = page.usage != null && showUsage ? Math.max(measureText(usageLabel).maxLineWidth, usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth))) : 1;
325
330
  let sectionMin = 1;
326
- if (hasContent(page.examples)) sectionMin = Math.max(sectionMin, ...label("Examples:", "examples").split("\n").map((line) => getDisplayWidth(line)));
327
- if (hasContent(page.author)) sectionMin = Math.max(sectionMin, ...label("Author:", "author").split("\n").map((line) => getDisplayWidth(line)));
328
- if (hasContent(page.bugs)) sectionMin = Math.max(sectionMin, ...label("Bugs:", "bugs").split("\n").map((line) => getDisplayWidth(line)));
331
+ for (const kind of [
332
+ "examples",
333
+ "author",
334
+ "bugs"
335
+ ]) {
336
+ if (!hasContent(page[kind])) continue;
337
+ const title = kind === "examples" ? "Examples:" : kind === "author" ? "Author:" : "Bugs:";
338
+ sectionMin = Math.max(sectionMin, 3, measureText(label(title, kind)).maxLineWidth);
339
+ }
329
340
  const minWidth = Math.max(entryMin, usageMin, sectionMin);
330
341
  if (options.maxWidth < minWidth) throw new RangeError(`maxWidth must be at least ${minWidth}, got ${options.maxWidth}.`);
331
342
  if (needsDescColumn && minDescWidth > 1) {
@@ -383,17 +394,21 @@ function formatDocPage(programName, page, options = {}) {
383
394
  output += sectionLabel;
384
395
  }
385
396
  for (const entry of section.entries) {
386
- const term = formatUsageTerm(entry.term, {
397
+ const termOptions = {
387
398
  colors: options.colors,
388
399
  theme: options.theme,
389
400
  optionsSeparator: ", ",
390
- context: "doc",
391
- maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
401
+ context: "doc"
402
+ };
403
+ const unwrappedTerm = formatUsageTerm(entry.term, termOptions);
404
+ const multilineReplacement = measureText(unwrappedTerm).lineCount > 1;
405
+ const term = options.maxWidth == null ? unwrappedTerm : formatUsageTerm(entry.term, {
406
+ ...termOptions,
407
+ maxWidth: options.maxWidth - termIndent
392
408
  });
393
409
  const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - effectiveTermWidth - 2;
394
- const termVisibleWidth = lastLineVisibleLength(term);
410
+ const termVisibleWidth = measureText(term).lastLineWidth;
395
411
  const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - effectiveTermWidth) : 0;
396
- const currentExtraOffset = () => description.includes("\n") ? 0 : extraTermOffset;
397
412
  const descFormatOptions = {
398
413
  colors: options.colors,
399
414
  quotes: !options.colors,
@@ -401,35 +416,26 @@ function formatDocPage(programName, page, options = {}) {
401
416
  initialWidth: extraTermOffset > 0 ? extraTermOffset : void 0
402
417
  };
403
418
  let description = entry.description == null ? "" : formatMessage(entry.description, descFormatOptions);
404
- if (options.showDefault && hasContent(entry.default)) {
405
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
406
- const suffix = punctuation(typeof options.showDefault === "object" ? options.showDefault.suffix ?? "]" : "]", "defaultSuffix");
407
- const prefixWidth = getDisplayWidth(prefix);
408
- const suffixWidth = getDisplayWidth(suffix);
409
- let defaultStartWidth;
410
- if (descColumnWidth != null) {
411
- const lastW = lastLineVisibleLength(description);
412
- const effectiveLastW = lastW + currentExtraOffset();
413
- if (effectiveLastW + prefixWidth >= descColumnWidth) {
414
- description += "\n";
415
- defaultStartWidth = prefixWidth;
416
- } else defaultStartWidth = effectiveLastW + prefixWidth;
417
- }
418
- const defaultFormatOptions = {
419
- colors: options.colors ? { resetSuffix: defaultStyle } : false,
420
- quotes: !options.colors,
421
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffixWidth,
422
- initialWidth: defaultStartWidth
423
- };
424
- const defaultContent = formatMessage(entry.default, defaultFormatOptions, options.theme?.annotationStyles?.default ?? { dim: true });
425
- const defaultText = `${prefix}${defaultContent}${opaqueFormatter && options.colors ? defaultStyle : ""}${suffix}`;
426
- const formattedDefault = options.colors ? `${defaultStyle}${defaultText}${defaultStyle ? "\x1B[0m" : ""}` : defaultText;
427
- description += formattedDefault;
428
- }
419
+ let cursor = placeText(description, {
420
+ line: "",
421
+ column: extraTermOffset
422
+ }).cursor;
423
+ const appendAnnotation = (content, layout, style, ambient, quotes) => {
424
+ const prefix = placeText(layout.prefix, cursor, descColumnWidth, 1);
425
+ if (prefix.text !== layout.prefix) description += "\n";
426
+ const rendered = formatMessage(content, {
427
+ colors: options.colors ? { resetSuffix: style } : false,
428
+ quotes,
429
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - layout.suffixMetrics.firstLineWidth,
430
+ initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
431
+ }, ambient);
432
+ const suffix = placeText(layout.suffix, placeText(rendered, prefix.cursor).cursor, descColumnWidth);
433
+ const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
434
+ description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
435
+ cursor = suffix.cursor;
436
+ };
437
+ if (options.showDefault && hasContent(entry.default)) appendAnnotation(entry.default, annotation("default"), defaultStyle, defaultAmbient, !options.colors);
429
438
  if (options.showChoices && hasContent(entry.choices)) {
430
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
431
- const suffix = punctuation(typeof options.showChoices === "object" ? options.showChoices.suffix ?? ")" : ")", "choicesSuffix");
432
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
433
439
  const maxItems = typeof options.showChoices === "object" ? options.showChoices.maxItems ?? 8 : 8;
434
440
  const terms = Array.isArray(entry.choices) ? entry.choices : [];
435
441
  let truncatedTerms = terms;
@@ -447,31 +453,10 @@ function formatDocPage(programName, page, options = {}) {
447
453
  }
448
454
  if (truncated) truncatedTerms = [...terms.slice(0, cutIndex), text(", ...")];
449
455
  }
450
- const choicesPrefixWidth = getDisplayWidth(prefix);
451
- const choicesSuffixWidth = getDisplayWidth(suffix);
452
- const choicesLabelWidth = getDisplayWidth(choicesLabelText);
453
- let choicesStartWidth;
454
- if (descColumnWidth != null) {
455
- const lastW = lastLineVisibleLength(description);
456
- const effectiveLastW = lastW + currentExtraOffset();
457
- const prefixLabelLen = choicesPrefixWidth + choicesLabelWidth;
458
- if (effectiveLastW + prefixLabelLen >= descColumnWidth) {
459
- description += "\n";
460
- choicesStartWidth = prefixLabelLen;
461
- } else choicesStartWidth = effectiveLastW + prefixLabelLen;
462
- }
463
- const choicesFormatOptions = {
464
- colors: options.colors ? { resetSuffix: choicesStyle } : false,
465
- quotes: false,
466
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - choicesSuffixWidth,
467
- initialWidth: choicesStartWidth
468
- };
469
- const choicesDisplay = formatMessage(truncatedTerms, choicesFormatOptions, options.theme?.annotationStyles?.choices ?? { dim: true });
470
- const choicesText = `${prefix}${choicesLabelText}${choicesDisplay}${opaqueFormatter && options.colors ? choicesStyle : ""}${suffix}`;
471
- const formattedChoices = options.colors ? `${choicesStyle}${choicesText}${choicesStyle ? "\x1B[0m" : ""}` : choicesText;
472
- description += formattedChoices;
456
+ appendAnnotation(truncatedTerms, annotation("choices"), choicesStyle, choicesAmbient, false);
473
457
  }
474
- output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, effectiveTermWidth)}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
458
+ const paddedTerm = ansiAwareRightPad(term, effectiveTermWidth);
459
+ output += `${" ".repeat(termIndent)}${multilineReplacement ? indentLines(paddedTerm, termIndent) : paddedTerm}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
475
460
  }
476
461
  }
477
462
  if (hasContent(page.examples)) {
@@ -530,21 +515,16 @@ function indentLines(text$1, indent) {
530
515
  * contribute to the rendered width.
531
516
  */
532
517
  function maxVisibleAtomicWidth(usage, theme) {
533
- return Math.max(0, ...usage.flatMap((term) => formatUsageTerm(term, {
518
+ return usage.reduce((widest, term) => Math.max(widest, measureText(formatUsageTerm(term, {
534
519
  theme,
535
520
  maxWidth: 1
536
- }).split("\n").map(getDisplayWidth)));
521
+ })).maxLineWidth), 0);
537
522
  }
538
523
  function ansiAwareRightPad(text$1, length, char = " ") {
539
- const visibleWidth = lastLineVisibleLength(text$1);
524
+ const visibleWidth = measureText(text$1).lastLineWidth;
540
525
  if (visibleWidth >= length) return text$1;
541
526
  return text$1 + char.repeat(length - visibleWidth);
542
527
  }
543
- function lastLineVisibleLength(text$1) {
544
- const lastNewline = text$1.lastIndexOf("\n");
545
- const lastLine = lastNewline === -1 ? text$1 : text$1.slice(lastNewline + 1);
546
- return getDisplayWidth(lastLine);
547
- }
548
528
 
549
529
  //#endregion
550
530
  export { cloneDocEntry, deduplicateDocEntries, deduplicateDocFragments, formatDocPage, isDocEntryHidden };
package/dist/facade.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_internal_annotations = require('./internal/annotations.cjs');
2
- const require_displaywidth = require('./displaywidth.cjs');
3
2
  const require_terminal_internal = require('./terminal-internal.cjs');
4
3
  const require_message = require('./message.cjs');
5
4
  const require_message_renderer = require('./message-renderer.cjs');
5
+ const require_text_layout = require('./text-layout.cjs');
6
6
  const require_validate = require('./validate.cjs');
7
7
  const require_usage = require('./usage.cjs');
8
8
  const require_doc = require('./doc.cjs');
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
521
521
  messageFormatter,
522
522
  theme
523
523
  });
524
- const errorLabel = () => require_terminal_internal.renderTerminalTerm({
524
+ const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
525
525
  type: "errorLabel",
526
526
  label: "Error:"
527
- }, theme, colors) + " ";
527
+ }, theme, colors));
528
528
  const shellName = completionArgs[0] || "";
529
529
  const args = completionArgs.slice(1);
530
530
  const callOnError = (code, error) => onError(code, error);
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
536
536
  colors,
537
537
  quotes: !colors,
538
538
  maxWidth,
539
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? "")
539
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth
540
540
  })}\n`);
541
541
  if (completionParser) {
542
542
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
570
570
  colors,
571
571
  quotes: !colors,
572
572
  maxWidth,
573
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? "")
573
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth
574
574
  })}`);
575
575
  return require_mode_dispatch.dispatchByMode(parser.mode, () => {
576
576
  const result = callOnError(1, error);
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
886
886
  messageFormatter,
887
887
  theme
888
888
  });
889
- const errorLabel = () => require_terminal_internal.renderTerminalTerm({
889
+ const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
890
890
  type: "errorLabel",
891
891
  label: "Error:"
892
- }, theme, colors) + " ";
892
+ }, theme, colors));
893
893
  let usagePrefix;
894
- const usageLabel = () => usagePrefix ??= require_terminal_internal.renderTerminalTerm({
894
+ const usageLabel = () => usagePrefix ??= require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
895
895
  type: "label",
896
896
  label: "Usage:",
897
897
  kind: "usageSummary"
898
- }, theme, colors) + " ";
899
- const usageLabelWidth = () => require_displaywidth.getDisplayWidth(usageLabel().split("\n").at(-1) ?? "");
898
+ }, theme, colors));
899
+ const usageLabelWidth = () => require_text_layout.measureText(usageLabel()).lastLineWidth;
900
900
  const norm = (c) => c === true ? {} : c;
901
901
  const helpCommandConfig = norm(options.help?.command);
902
902
  const helpOptionConfig = norm(options.help?.option);
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1059
1059
  }), usageLabelWidth())}`);
1060
1060
  const prefix = errorLabel();
1061
1061
  const errorMessage = formatMessage(validationError, {
1062
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1062
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1063
1063
  maxWidth,
1064
1064
  colors,
1065
1065
  quotes: !colors
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1179
1179
  }), usageLabelWidth())}`);
1180
1180
  const prefix = errorLabel();
1181
1181
  const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1182
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1183
1183
  maxWidth,
1184
1184
  colors,
1185
1185
  quotes: !colors
package/dist/facade.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { injectAnnotations } from "./internal/annotations.js";
2
- import { getDisplayWidth } from "./displaywidth.js";
3
2
  import { renderTerminalTerm } from "./terminal-internal.js";
4
3
  import { commandLine, lineBreak, message, optionName, text, value } from "./message.js";
5
4
  import { resolveMessageFormatter } from "./message-renderer.js";
5
+ import { measureText, spaceAfterLabel } from "./text-layout.js";
6
6
  import { validateCommandNames, validateContextIds, validateMetaNameCollisions, validateOptionNames, validateProgramName } from "./validate.js";
7
7
  import { cloneUsage, formatUsage, isSuggestionHidden, normalizeUsage } from "./usage.js";
8
8
  import { formatDocPage } from "./doc.js";
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
521
521
  messageFormatter,
522
522
  theme
523
523
  });
524
- const errorLabel = () => renderTerminalTerm({
524
+ const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
525
525
  type: "errorLabel",
526
526
  label: "Error:"
527
- }, theme, colors) + " ";
527
+ }, theme, colors));
528
528
  const shellName = completionArgs[0] || "";
529
529
  const args = completionArgs.slice(1);
530
530
  const callOnError = (code, error) => onError(code, error);
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
536
536
  colors,
537
537
  quotes: !colors,
538
538
  maxWidth,
539
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? "")
539
+ initialWidth: measureText(prefix).lastLineWidth
540
540
  })}\n`);
541
541
  if (completionParser) {
542
542
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
570
570
  colors,
571
571
  quotes: !colors,
572
572
  maxWidth,
573
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? "")
573
+ initialWidth: measureText(prefix).lastLineWidth
574
574
  })}`);
575
575
  return dispatchByMode(parser.mode, () => {
576
576
  const result = callOnError(1, error);
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
886
886
  messageFormatter,
887
887
  theme
888
888
  });
889
- const errorLabel = () => renderTerminalTerm({
889
+ const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
890
890
  type: "errorLabel",
891
891
  label: "Error:"
892
- }, theme, colors) + " ";
892
+ }, theme, colors));
893
893
  let usagePrefix;
894
- const usageLabel = () => usagePrefix ??= renderTerminalTerm({
894
+ const usageLabel = () => usagePrefix ??= spaceAfterLabel(renderTerminalTerm({
895
895
  type: "label",
896
896
  label: "Usage:",
897
897
  kind: "usageSummary"
898
- }, theme, colors) + " ";
899
- const usageLabelWidth = () => getDisplayWidth(usageLabel().split("\n").at(-1) ?? "");
898
+ }, theme, colors));
899
+ const usageLabelWidth = () => measureText(usageLabel()).lastLineWidth;
900
900
  const norm = (c) => c === true ? {} : c;
901
901
  const helpCommandConfig = norm(options.help?.command);
902
902
  const helpOptionConfig = norm(options.help?.option);
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1059
1059
  }), usageLabelWidth())}`);
1060
1060
  const prefix = errorLabel();
1061
1061
  const errorMessage = formatMessage(validationError, {
1062
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1062
+ initialWidth: measureText(prefix).lastLineWidth,
1063
1063
  maxWidth,
1064
1064
  colors,
1065
1065
  quotes: !colors
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1179
1179
  }), usageLabelWidth())}`);
1180
1180
  const prefix = errorLabel();
1181
1181
  const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1182
+ initialWidth: measureText(prefix).lastLineWidth,
1183
1183
  maxWidth,
1184
1184
  colors,
1185
1185
  quotes: !colors
@@ -1,7 +1,11 @@
1
1
  const require_displaywidth = require('../displaywidth.cjs');
2
2
  const require_terminal_internal = require('../terminal-internal.cjs');
3
3
  const require_message_renderer = require('../message-renderer.cjs');
4
+ const require_text_layout = require('../text-layout.cjs');
4
5
 
5
6
  exports.getDisplayWidth = require_displaywidth.getDisplayWidth;
7
+ exports.measureText = require_text_layout.measureText;
8
+ exports.placeText = require_text_layout.placeText;
6
9
  exports.renderTerminalTerm = require_terminal_internal.renderTerminalTerm;
7
- exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
10
+ exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
11
+ exports.spaceAfterLabel = require_text_layout.spaceAfterLabel;
@@ -1,4 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.cjs";
2
2
  import { resolveMessageFormatter } from "../message-renderer.cjs";
3
3
  import { renderTerminalTerm } from "../terminal-internal.cjs";
4
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.cjs";
5
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,4 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
2
  import { resolveMessageFormatter } from "../message-renderer.js";
3
3
  import { renderTerminalTerm } from "../terminal-internal.js";
4
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
5
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,5 +1,6 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
2
  import { renderTerminalTerm } from "../terminal-internal.js";
3
3
  import { resolveMessageFormatter } from "../message-renderer.js";
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
4
5
 
5
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
6
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -10,6 +10,8 @@ interface TerminalToken {
10
10
  readonly text: string;
11
11
  readonly width: number;
12
12
  readonly scopes: readonly Scope[];
13
+ /** Layout-owned space, removable beside an explicit hard break. */
14
+ readonly separator?: boolean;
13
15
  }
14
16
  declare function terminalText(text: string): TerminalFragment;
15
17
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -10,6 +10,8 @@ interface TerminalToken {
10
10
  readonly text: string;
11
11
  readonly width: number;
12
12
  readonly scopes: readonly Scope[];
13
+ /** Layout-owned space, removable beside an explicit hard break. */
14
+ readonly separator?: boolean;
13
15
  }
14
16
  declare function terminalText(text: string): TerminalFragment;
15
17
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -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
@@ -196,8 +196,8 @@ function formatUsage(programName, usage, options = {}) {
196
196
  if (usage.length > 0 && usage.slice(0, -1).every((t) => t.type === "command") && lastTerm.type === "exclusive" && lastTerm.terms.every((t) => t.length > 0 && (t[0].type === "command" || t[0].type === "option" || t[0].type === "argument" || t[0].type === "optional" && t[0].terms.length === 1 && (t[0].terms[0].type === "command" || t[0].terms[0].type === "option" || t[0].terms[0].type === "argument")))) {
197
197
  const lines = [];
198
198
  for (let command of lastTerm.terms) {
199
- const firstTerm = command[0];
200
- if (firstTerm?.type === "command" && isUsageHidden(firstTerm.hidden)) continue;
199
+ const firstTerm$1 = command[0];
200
+ if (firstTerm$1?.type === "command" && isUsageHidden(firstTerm$1.hidden)) continue;
201
201
  if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
202
202
  lines.push(formatUsage(programName, command, options));
203
203
  }
@@ -208,10 +208,13 @@ function formatUsage(programName, usage, options = {}) {
208
208
  type: "programName",
209
209
  programName
210
210
  }, options.theme)), options);
211
- const terms = [...formatUsageTerms(usage, options)];
211
+ const terms = [...normalizeUsageSeparators(formatUsageTerms(usage, options))];
212
212
  if (terms.length === 0) return require_terminal_internal.serializeTokens(programTokens, options.colors);
213
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 ? {
214
+ const firstTerm = terms.find((token) => token.text !== "");
215
+ const lastProgram = programTokens.findLast((token) => token.text !== "");
216
+ if (lastProgram == null || firstTerm == null || lastProgram.width === -1 || firstTerm.width === -1) return require_terminal_internal.serializeTokens(programTokens, options.colors) + wrapUsageTokens(terms, options, width);
217
+ const separator = options.maxWidth != null && width + 1 + firstTerm.width > options.maxWidth ? {
215
218
  text: "\n",
216
219
  width: -1,
217
220
  scopes: []
@@ -456,7 +459,8 @@ function* formatUsageTerms(terms, options) {
456
459
  if (i > 0) yield {
457
460
  text: " ",
458
461
  width: 1,
459
- scopes: []
462
+ scopes: [],
463
+ separator: true
460
464
  };
461
465
  yield* formatUsageTermInternal(t, options);
462
466
  i++;
@@ -486,16 +490,37 @@ function formatUsageTerm(term, options = {}) {
486
490
  function wrapUsageTokens(input, options, initialWidth = 0) {
487
491
  return require_terminal_internal.serializeTokens(layoutUsageTokens(input, options, initialWidth), options.colors);
488
492
  }
493
+ /** Keeps one layout-owned space between nonempty outputs on the same line. */
494
+ function* normalizeUsageSeparators(input) {
495
+ let previous;
496
+ let pending = [];
497
+ for (const token of input) {
498
+ if (token.separator || token.text === "") {
499
+ pending.push(token);
500
+ continue;
501
+ }
502
+ let separated = false;
503
+ for (const item of pending) if (!item.separator) yield item;
504
+ else if (!separated && previous != null && previous.width !== -1 && token.width !== -1) {
505
+ yield item;
506
+ separated = true;
507
+ }
508
+ pending = [];
509
+ yield token;
510
+ previous = token;
511
+ }
512
+ for (const item of pending) if (!item.separator) yield item;
513
+ }
489
514
  function layoutUsageTokens(input, options, initialWidth = 0) {
490
515
  let lineWidth = initialWidth;
491
516
  const output = [];
492
- for (const token of input) {
517
+ for (const token of normalizeUsageSeparators(input)) {
493
518
  if (token.width === -1) {
494
519
  output.push(token);
495
520
  lineWidth = 0;
496
521
  continue;
497
522
  }
498
- if (options.maxWidth != null && lineWidth > 0 && lineWidth + token.width > options.maxWidth) {
523
+ if (options.maxWidth != null && lineWidth > 0 && token.width > 0 && lineWidth + token.width > options.maxWidth) {
499
524
  const last = output.at(-1);
500
525
  if (last?.text.endsWith(" ") && (!options.colors || last.scopes.length === 0)) output[output.length - 1] = {
501
526
  ...last,
@@ -529,7 +554,8 @@ function* formatUsageTermInternal(term, options) {
529
554
  const space = {
530
555
  text: " ",
531
556
  width: 1,
532
- scopes: []
557
+ scopes: [],
558
+ separator: true
533
559
  };
534
560
  if (term.type === "argument") yield* leaf({
535
561
  type: "metavar",
package/dist/usage.js CHANGED
@@ -196,8 +196,8 @@ function formatUsage(programName, usage, options = {}) {
196
196
  if (usage.length > 0 && usage.slice(0, -1).every((t) => t.type === "command") && lastTerm.type === "exclusive" && lastTerm.terms.every((t) => t.length > 0 && (t[0].type === "command" || t[0].type === "option" || t[0].type === "argument" || t[0].type === "optional" && t[0].terms.length === 1 && (t[0].terms[0].type === "command" || t[0].terms[0].type === "option" || t[0].terms[0].type === "argument")))) {
197
197
  const lines = [];
198
198
  for (let command of lastTerm.terms) {
199
- const firstTerm = command[0];
200
- if (firstTerm?.type === "command" && isUsageHidden(firstTerm.hidden)) continue;
199
+ const firstTerm$1 = command[0];
200
+ if (firstTerm$1?.type === "command" && isUsageHidden(firstTerm$1.hidden)) continue;
201
201
  if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
202
202
  lines.push(formatUsage(programName, command, options));
203
203
  }
@@ -208,10 +208,13 @@ function formatUsage(programName, usage, options = {}) {
208
208
  type: "programName",
209
209
  programName
210
210
  }, options.theme)), options);
211
- const terms = [...formatUsageTerms(usage, options)];
211
+ const terms = [...normalizeUsageSeparators(formatUsageTerms(usage, options))];
212
212
  if (terms.length === 0) return serializeTokens(programTokens, options.colors);
213
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 ? {
214
+ const firstTerm = terms.find((token) => token.text !== "");
215
+ const lastProgram = programTokens.findLast((token) => token.text !== "");
216
+ if (lastProgram == null || firstTerm == null || lastProgram.width === -1 || firstTerm.width === -1) return serializeTokens(programTokens, options.colors) + wrapUsageTokens(terms, options, width);
217
+ const separator = options.maxWidth != null && width + 1 + firstTerm.width > options.maxWidth ? {
215
218
  text: "\n",
216
219
  width: -1,
217
220
  scopes: []
@@ -456,7 +459,8 @@ function* formatUsageTerms(terms, options) {
456
459
  if (i > 0) yield {
457
460
  text: " ",
458
461
  width: 1,
459
- scopes: []
462
+ scopes: [],
463
+ separator: true
460
464
  };
461
465
  yield* formatUsageTermInternal(t, options);
462
466
  i++;
@@ -486,16 +490,37 @@ function formatUsageTerm(term, options = {}) {
486
490
  function wrapUsageTokens(input, options, initialWidth = 0) {
487
491
  return serializeTokens(layoutUsageTokens(input, options, initialWidth), options.colors);
488
492
  }
493
+ /** Keeps one layout-owned space between nonempty outputs on the same line. */
494
+ function* normalizeUsageSeparators(input) {
495
+ let previous;
496
+ let pending = [];
497
+ for (const token of input) {
498
+ if (token.separator || token.text === "") {
499
+ pending.push(token);
500
+ continue;
501
+ }
502
+ let separated = false;
503
+ for (const item of pending) if (!item.separator) yield item;
504
+ else if (!separated && previous != null && previous.width !== -1 && token.width !== -1) {
505
+ yield item;
506
+ separated = true;
507
+ }
508
+ pending = [];
509
+ yield token;
510
+ previous = token;
511
+ }
512
+ for (const item of pending) if (!item.separator) yield item;
513
+ }
489
514
  function layoutUsageTokens(input, options, initialWidth = 0) {
490
515
  let lineWidth = initialWidth;
491
516
  const output = [];
492
- for (const token of input) {
517
+ for (const token of normalizeUsageSeparators(input)) {
493
518
  if (token.width === -1) {
494
519
  output.push(token);
495
520
  lineWidth = 0;
496
521
  continue;
497
522
  }
498
- if (options.maxWidth != null && lineWidth > 0 && lineWidth + token.width > options.maxWidth) {
523
+ if (options.maxWidth != null && lineWidth > 0 && token.width > 0 && lineWidth + token.width > options.maxWidth) {
499
524
  const last = output.at(-1);
500
525
  if (last?.text.endsWith(" ") && (!options.colors || last.scopes.length === 0)) output[output.length - 1] = {
501
526
  ...last,
@@ -529,7 +554,8 @@ function* formatUsageTermInternal(term, options) {
529
554
  const space = {
530
555
  text: " ",
531
556
  width: 1,
532
- scopes: []
557
+ scopes: [],
558
+ separator: true
533
559
  };
534
560
  if (term.type === "argument") yield* leaf({
535
561
  type: "metavar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.3.0-dev.2542",
3
+ "version": "1.3.0-dev.2554",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "sideEffects": false,
239
239
  "devDependencies": {
240
- "@optique/env": "1.3.0-dev.2542+1201b1a0",
240
+ "@optique/env": "1.3.0-dev.2554+5fdcbbad",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",