@promptctl/rich-js 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +13 -26
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +0 -1
  7. package/dist/core/box.js +6 -18
  8. package/dist/core/cells.d.ts +16 -5
  9. package/dist/core/cells.js +31 -10
  10. package/dist/core/color.d.ts +4 -8
  11. package/dist/core/color.js +28 -19
  12. package/dist/core/console.d.ts +13 -4
  13. package/dist/core/console.js +91 -36
  14. package/dist/core/export-html.d.ts +40 -5
  15. package/dist/core/export-html.js +111 -17
  16. package/dist/core/markup.d.ts +27 -5
  17. package/dist/core/markup.js +282 -170
  18. package/dist/core/measure.d.ts +1 -1
  19. package/dist/core/measure.js +2 -1
  20. package/dist/core/place.d.ts +30 -0
  21. package/dist/core/place.js +51 -0
  22. package/dist/core/pretty.d.ts +3 -0
  23. package/dist/core/pretty.js +5 -1
  24. package/dist/core/protocol.d.ts +136 -6
  25. package/dist/core/protocol.js +64 -2
  26. package/dist/core/render.d.ts +2 -2
  27. package/dist/core/render.js +2 -2
  28. package/dist/core/segment.d.ts +23 -4
  29. package/dist/core/segment.js +75 -16
  30. package/dist/core/spinnerData.d.ts +2 -0
  31. package/dist/core/spinnerData.js +3 -1
  32. package/dist/core/strip.d.ts +3 -7
  33. package/dist/core/strip.js +32 -35
  34. package/dist/core/style.d.ts +25 -0
  35. package/dist/core/style.js +61 -89
  36. package/dist/core/text.d.ts +16 -15
  37. package/dist/core/text.js +62 -76
  38. package/dist/host/app.d.ts +121 -0
  39. package/dist/host/app.js +255 -0
  40. package/dist/host/host-environment.d.ts +26 -0
  41. package/dist/host/host-environment.js +44 -0
  42. package/dist/host/index.d.ts +4 -2
  43. package/dist/host/index.js +2 -1
  44. package/dist/host/terminal-host.d.ts +43 -7
  45. package/dist/host/terminal-host.js +57 -3
  46. package/dist/index.d.ts +7 -3
  47. package/dist/index.js +4 -1
  48. package/dist/node/terminal-host.d.ts +31 -2
  49. package/dist/node/terminal-host.js +69 -2
  50. package/dist/node/traceback.d.ts +16 -6
  51. package/dist/node/traceback.js +30 -8
  52. package/dist/renderables/align.d.ts +2 -2
  53. package/dist/renderables/align.js +6 -19
  54. package/dist/renderables/columns.js +4 -20
  55. package/dist/renderables/embed.d.ts +50 -0
  56. package/dist/renderables/embed.js +71 -0
  57. package/dist/renderables/group.js +3 -1
  58. package/dist/renderables/layout.js +31 -34
  59. package/dist/renderables/live.d.ts +6 -3
  60. package/dist/renderables/live.js +89 -45
  61. package/dist/renderables/markdown.d.ts +1 -1
  62. package/dist/renderables/markdown.js +6 -4
  63. package/dist/renderables/padding.js +3 -2
  64. package/dist/renderables/panel.js +26 -41
  65. package/dist/renderables/progress.js +5 -8
  66. package/dist/renderables/progressBar.js +6 -5
  67. package/dist/renderables/rule.d.ts +3 -2
  68. package/dist/renderables/rule.js +9 -9
  69. package/dist/renderables/spinner.d.ts +5 -3
  70. package/dist/renderables/spinner.js +14 -8
  71. package/dist/renderables/syntax.d.ts +1 -1
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +26 -14
  74. package/dist/renderables/table.js +239 -141
  75. package/dist/renderables/traceback.js +8 -9
  76. package/dist/renderables/tree.d.ts +17 -3
  77. package/dist/renderables/tree.js +62 -71
  78. package/dist/renderables/viewport.d.ts +114 -0
  79. package/dist/renderables/viewport.js +198 -0
  80. package/dist/template-bindings/color-funcs.js +11 -13
  81. package/dist/template-bindings/index.js +4 -2
  82. package/dist/template-bindings/palette-funcs.js +6 -16
  83. package/dist/template-bindings/style-funcs.js +4 -0
  84. package/dist/themes/data/ansi-tables.d.ts +14 -0
  85. package/dist/themes/data/ansi-tables.js +49 -0
  86. package/dist/themes/data/atom-one-dark.js +19 -0
  87. package/dist/themes/data/atom-one-light.js +19 -0
  88. package/dist/themes/data/catppuccin-frappe.js +19 -0
  89. package/dist/themes/data/catppuccin-latte.js +19 -0
  90. package/dist/themes/data/catppuccin-macchiato.js +19 -0
  91. package/dist/themes/data/catppuccin-mocha.js +19 -0
  92. package/dist/themes/data/cyberpunk.js +2 -0
  93. package/dist/themes/data/default.js +2 -0
  94. package/dist/themes/data/dracula.js +19 -0
  95. package/dist/themes/data/flexoki.js +19 -0
  96. package/dist/themes/data/gruvbox.js +19 -0
  97. package/dist/themes/data/monokai.js +19 -0
  98. package/dist/themes/data/nord.js +19 -0
  99. package/dist/themes/data/rose-pine-dawn.js +19 -0
  100. package/dist/themes/data/rose-pine-moon.js +19 -0
  101. package/dist/themes/data/rose-pine.js +19 -0
  102. package/dist/themes/data/solarized-dark.js +19 -0
  103. package/dist/themes/data/solarized-light.js +19 -0
  104. package/dist/themes/data/svg-export.js +19 -0
  105. package/dist/themes/data/textual-ansi.js +2 -0
  106. package/dist/themes/data/textual-dark.js +2 -0
  107. package/dist/themes/data/textual-light.js +2 -0
  108. package/dist/themes/data/tokyo-night.js +19 -0
  109. package/dist/themes/data/types.d.ts +16 -0
  110. package/dist/themes/data/types.js +10 -1
  111. package/dist/themes/registry.d.ts +3 -2
  112. package/dist/themes/registry.js +13 -1
  113. package/dist/themes/terminalThemes.d.ts +2 -7
  114. package/dist/themes/terminalThemes.js +4 -9
  115. package/dist/widgets/button.d.ts +1 -1
  116. package/dist/widgets/button.js +4 -2
  117. package/dist/widgets/checkbox.d.ts +2 -2
  118. package/dist/widgets/checkbox.js +5 -3
  119. package/dist/widgets/dropdown.d.ts +2 -2
  120. package/dist/widgets/dropdown.js +13 -16
  121. package/dist/widgets/event-router.d.ts +21 -28
  122. package/dist/widgets/event-router.js +80 -73
  123. package/dist/widgets/focus-manager.d.ts +25 -6
  124. package/dist/widgets/focus-manager.js +63 -65
  125. package/dist/widgets/hit.d.ts +64 -0
  126. package/dist/widgets/hit.js +113 -0
  127. package/dist/widgets/index.d.ts +5 -7
  128. package/dist/widgets/index.js +3 -4
  129. package/dist/widgets/slider.d.ts +2 -2
  130. package/dist/widgets/slider.js +7 -7
  131. package/dist/widgets/static-item.d.ts +9 -15
  132. package/dist/widgets/static-item.js +9 -15
  133. package/dist/widgets/text-input.d.ts +2 -2
  134. package/dist/widgets/text-input.js +17 -14
  135. package/dist/widgets/toggle.d.ts +1 -1
  136. package/dist/widgets/toggle.js +3 -2
  137. package/dist/widgets/types.d.ts +14 -35
  138. package/dist/widgets/types.js +0 -1
  139. package/dist/widgets/widget-app.d.ts +63 -0
  140. package/dist/widgets/widget-app.js +199 -0
  141. package/dist/widgets/widget-base.d.ts +4 -6
  142. package/dist/widgets/widget-base.js +22 -30
  143. package/package.json +12 -4
  144. package/dist/host/host-stream.d.ts +0 -30
  145. package/dist/host/host-stream.js +0 -35
  146. package/dist/widgets/screen.d.ts +0 -98
  147. package/dist/widgets/screen.js +0 -530
@@ -7,8 +7,9 @@ import { NULL_STYLE } from "../core/style.js";
7
7
  import { ROUNDED } from "../core/box.js";
8
8
  import { Measurement } from "../core/measure.js";
9
9
  import { RichText } from "../core/text.js";
10
+ import { embed, inlineLabel } from "./embed.js";
10
11
  import { normalizePadding } from "./padding.js";
11
- import { getStyle, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
+ import { fitHeight, getStyle, insetHeight, isMeasurable, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
13
  function layoutPanel(outerWidth, padding) {
13
14
  const [, padRightWanted, , padLeftWanted] = padding;
14
15
  // Panel's one division point, and the width reaching it is not always one
@@ -54,22 +55,6 @@ function frameOverhead(geometry) {
54
55
  function borderTextStyle(options, own, border) {
55
56
  return own === undefined ? border : getStyle(options, own);
56
57
  }
57
- function toRenderable(content) {
58
- // [LAW:one-type-per-behavior] Content embedded in a panel is a line, not a
59
- // printed unit — the same reason columns, table cells, tree labels, and
60
- // every other embedding site build their RichText with `end: ""`. Now that
61
- // `RichText.render` honors `end` uniformly (rich-text-5ai), leaving this at
62
- // the default "\n" would draw a trailing blank row whenever content ends
63
- // with a literal newline.
64
- if (typeof content === "string")
65
- return new RichText(content, { end: "" });
66
- if (content instanceof RichText) {
67
- const copy = content.copy();
68
- copy.end = "";
69
- return copy;
70
- }
71
- return content;
72
- }
73
58
  export class Panel {
74
59
  renderable;
75
60
  box;
@@ -84,7 +69,7 @@ export class Panel {
84
69
  width;
85
70
  padding;
86
71
  constructor(content, options) {
87
- this.renderable = toRenderable(content);
72
+ this.renderable = embed(content);
88
73
  this.box = options?.box ?? ROUNDED;
89
74
  this.title = options?.title;
90
75
  this.subtitle = options?.subtitle;
@@ -99,7 +84,7 @@ export class Panel {
99
84
  }
100
85
  *render(rawOptions) {
101
86
  const options = withBoundedWidth(rawOptions, this);
102
- const box = options.asciiOnly ? this.box.substitute({ asciiOnly: true }) : this.box;
87
+ const box = this.box.substitute(options);
103
88
  const borderStyle = getStyle(options, this.borderStyle);
104
89
  const style = getStyle(options, this.style);
105
90
  const border = borderStyle.isNull ? undefined : borderStyle;
@@ -129,11 +114,13 @@ export class Panel {
129
114
  * fires at every width and reads state this render populates.
130
115
  */
131
116
  _renderContent(options, contentWidth) {
132
- const innerOptions = {
133
- ...options,
134
- maxWidth: contentWidth,
135
- };
136
- const lines = Segment.splitLines([...this.renderable.render(innerOptions)]);
117
+ // Two border rows and the vertical padding are the panel's own. Handed on
118
+ // as a region, the rest is the panel's to shape, which is what stretches
119
+ // its frame down a pane and keeps its bottom border inside one.
120
+ const [padTop, , padBottom] = this.padding;
121
+ const height = insetHeight(options.height, 2 + padTop + padBottom);
122
+ const innerOptions = { ...options, maxWidth: contentWidth, height };
123
+ const lines = fitHeight(Segment.splitLines([...this.renderable.render(innerOptions)]), height);
137
124
  return contentWidth === 0 ? [] : lines;
138
125
  }
139
126
  /**
@@ -225,23 +212,22 @@ export class Panel {
225
212
  }
226
213
  *_renderTopBorder(options, box, geometry, border) {
227
214
  const innerBorderWidth = geometry.spanWidth;
228
- if (!this.title) {
215
+ const titleSeg = borderTextStyle(options, this.titleStyle, border);
216
+ const title = inlineLabel(this.title, options, titleSeg);
217
+ const titleWidth = Segment.getLineLength(title);
218
+ if (titleWidth === 0) {
229
219
  yield new Segment(box.top.left.repeat(geometry.left), border);
230
220
  yield new Segment(box.top.horizontal.repeat(innerBorderWidth), border);
231
221
  yield new Segment(box.top.right.repeat(geometry.right), border);
232
222
  yield Segment.line();
233
223
  return;
234
224
  }
235
- const titleText = typeof this.title === "string" ? this.title : this.title.plain;
236
- const titleDisplay = ` ${titleText} `;
237
- const titleWidth = cellLen(titleDisplay);
238
- const titleSeg = borderTextStyle(options, this.titleStyle, border);
239
225
  yield new Segment(box.top.left.repeat(geometry.left), border);
240
226
  if (titleWidth >= innerBorderWidth) {
241
- // Title fills the border. [LAW:one-source-of-truth] cellLen / setCellSize
242
- // are the cell-width authority — plain .slice would miscount wide chars
243
- // and break border alignment.
244
- yield new Segment(setCellSize(titleDisplay, asCellCol(innerBorderWidth)), titleSeg);
227
+ // Title fills the border. [LAW:one-source-of-truth] adjustLineLength is
228
+ // cell-aware — a plain .slice would miscount wide chars and break border
229
+ // alignment.
230
+ yield* Segment.adjustLineLength(title, innerBorderWidth, titleSeg);
245
231
  }
246
232
  else {
247
233
  // Center the title in the top border
@@ -249,7 +235,7 @@ export class Panel {
249
235
  const rightRuleWidth = innerBorderWidth - titleWidth - leftRuleWidth;
250
236
  if (leftRuleWidth > 0)
251
237
  yield new Segment(box.top.horizontal.repeat(leftRuleWidth), border);
252
- yield new Segment(titleDisplay, titleSeg);
238
+ yield* title;
253
239
  if (rightRuleWidth > 0)
254
240
  yield new Segment(box.top.horizontal.repeat(rightRuleWidth), border);
255
241
  }
@@ -275,25 +261,24 @@ export class Panel {
275
261
  // (if any) hugs the right edge and the subtitle treats the remainder
276
262
  // as its centering canvas.
277
263
  const centerWidth = Math.max(0, innerBorderWidth - accessoryWidth);
278
- if (!this.subtitle) {
264
+ const subtitleSeg = borderTextStyle(options, this.subtitleStyle, border);
265
+ const subtitle = inlineLabel(this.subtitle, options, subtitleSeg);
266
+ const subtitleWidth = Segment.getLineLength(subtitle);
267
+ if (subtitleWidth === 0) {
279
268
  if (centerWidth > 0)
280
269
  yield new Segment(box.bottom.horizontal.repeat(centerWidth), border);
281
270
  }
282
271
  else {
283
- const subtitleText = typeof this.subtitle === "string" ? this.subtitle : this.subtitle.plain;
284
- const subtitleDisplay = ` ${subtitleText} `;
285
- const subtitleWidth = cellLen(subtitleDisplay);
286
- const subtitleSeg = borderTextStyle(options, this.subtitleStyle, border);
287
272
  if (subtitleWidth >= centerWidth) {
288
273
  // Cell-aware clip — see _renderTopBorder.
289
- yield new Segment(setCellSize(subtitleDisplay, asCellCol(centerWidth)), subtitleSeg);
274
+ yield* Segment.adjustLineLength(subtitle, centerWidth, subtitleSeg);
290
275
  }
291
276
  else {
292
277
  const leftRuleWidth = Math.floor((centerWidth - subtitleWidth) / 2);
293
278
  const rightRuleWidth = centerWidth - subtitleWidth - leftRuleWidth;
294
279
  if (leftRuleWidth > 0)
295
280
  yield new Segment(box.bottom.horizontal.repeat(leftRuleWidth), border);
296
- yield new Segment(subtitleDisplay, subtitleSeg);
281
+ yield* subtitle;
297
282
  if (rightRuleWidth > 0)
298
283
  yield new Segment(box.bottom.horizontal.repeat(rightRuleWidth), border);
299
284
  }
@@ -4,7 +4,8 @@
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { RichText } from "../core/text.js";
6
6
  import { Console } from "../core/console.js";
7
- import { escape as escapeMarkup, renderMarkup } from "../core/markup.js";
7
+ import { escape as escapeMarkup } from "../core/markup.js";
8
+ import { embeddedText } from "./embed.js";
8
9
  import { ProgressBar } from "./progressBar.js";
9
10
  import { Spinner } from "./spinner.js";
10
11
  import { Live } from "./live.js";
@@ -23,13 +24,9 @@ export class TextColumn {
23
24
  // Use a callback so `$&`/`$1`/`$$` in the task description aren't
24
25
  // reinterpreted by String.replace as replacement patterns.
25
26
  const formatted = this.format.replace(/\{task\.description\}/g, () => description);
26
- // [LAW:one-type-per-behavior] A column is a row fragment, not a printed
27
- // line, so it clears `end` the same way `toCellText` does in table.ts —
28
- // `renderMarkup` returns a fresh `RichText` each call, so it is safe to
29
- // mutate directly rather than copy.
30
- const text = renderMarkup(formatted);
31
- text.end = "";
32
- yield* text.render(options);
27
+ // [LAW:single-enforcer] A column is a row fragment, the same as any
28
+ // embedded text.
29
+ yield* embeddedText(formatted).render(options);
33
30
  }
34
31
  }
35
32
  export class BarColumn {
@@ -3,9 +3,9 @@
3
3
  */
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { NULL_STYLE } from "../core/style.js";
6
- import { getStyle } from "../core/protocol.js";
7
- const FULL_BLOCK = "━";
8
- const EMPTY_BLOCK = "━";
6
+ import { drawable, getStyle } from "../core/protocol.js";
7
+ const BAR = "━";
8
+ const ASCII_BAR = "-";
9
9
  export class ProgressBar {
10
10
  total;
11
11
  completed;
@@ -38,11 +38,12 @@ export class ProgressBar {
38
38
  const back = getStyle(options, this.style);
39
39
  const fillStyle = fill.isNull ? undefined : fill;
40
40
  const bgStyle = back.isNull ? undefined : back;
41
+ const bar = drawable(options, BAR, ASCII_BAR);
41
42
  if (filledWidth > 0) {
42
- yield new Segment(FULL_BLOCK.repeat(filledWidth), fillStyle);
43
+ yield new Segment(bar.repeat(filledWidth), fillStyle);
43
44
  }
44
45
  if (emptyWidth > 0) {
45
- yield new Segment(EMPTY_BLOCK.repeat(emptyWidth), bgStyle);
46
+ yield new Segment(bar.repeat(emptyWidth), bgStyle);
46
47
  }
47
48
  }
48
49
  measure(_options) {
@@ -2,6 +2,7 @@
2
2
  * Rule — a horizontal divider line, optionally with a centered title.
3
3
  */
4
4
  import { Segment } from "../core/segment.js";
5
+ import type { RichText } from "../core/text.js";
5
6
  import { Style } from "../core/style.js";
6
7
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
8
  export type RuleAlign = "left" | "center" | "right";
@@ -11,11 +12,11 @@ export interface RuleOptions {
11
12
  style?: string | Style;
12
13
  }
13
14
  export declare class Rule implements Renderable, Measurable {
14
- readonly title: string | undefined;
15
+ readonly title: string | RichText | undefined;
15
16
  readonly characters: string;
16
17
  readonly align: RuleAlign;
17
18
  readonly style: string | Style;
18
- constructor(title?: string, options?: RuleOptions);
19
+ constructor(title?: string | RichText, options?: RuleOptions);
19
20
  render(options: RenderOptions): Iterable<Segment>;
20
21
  measure(_options: RenderOptions): {
21
22
  minimum: number;
@@ -4,7 +4,8 @@
4
4
  import { cellLen } from "../core/cells.js";
5
5
  import { Segment } from "../core/segment.js";
6
6
  import { NULL_STYLE } from "../core/style.js";
7
- import { getStyle } from "../core/protocol.js";
7
+ import { drawable, getStyle } from "../core/protocol.js";
8
+ import { inlineLabel } from "./embed.js";
8
9
  const ASCII_RULE_CHAR = "-";
9
10
  const DEFAULT_RULE_CHAR = "\u2500"; // ─
10
11
  export class Rule {
@@ -28,21 +29,20 @@ export class Rule {
28
29
  }
29
30
  *render(options) {
30
31
  const maxWidth = options.maxWidth;
31
- const ruleChar = options.asciiOnly ? ASCII_RULE_CHAR : this.characters;
32
+ const ruleChar = drawable(options, this.characters, ASCII_RULE_CHAR);
32
33
  const style = getStyle(options, this.style);
33
34
  const ruleStyle = style.isNull ? undefined : style;
34
- if (!this.title) {
35
+ const title = inlineLabel(this.title, options, ruleStyle);
36
+ const titleWidth = Segment.getLineLength(title);
37
+ if (titleWidth === 0) {
35
38
  // No title — just a line of repeated characters
36
39
  yield new Segment(repeatToWidth(ruleChar, maxWidth), ruleStyle);
37
40
  yield Segment.line();
38
41
  return;
39
42
  }
40
- // With title
41
- const titleText = ` ${this.title} `;
42
- const titleWidth = cellLen(titleText);
43
43
  if (titleWidth >= maxWidth) {
44
- // Title fills the whole width
45
- yield new Segment(titleText.slice(0, maxWidth), ruleStyle);
44
+ // Title fills the whole width; adjustLineLength cuts by cells, not code units.
45
+ yield* Segment.adjustLineLength(title, maxWidth, ruleStyle);
46
46
  yield Segment.line();
47
47
  return;
48
48
  }
@@ -57,7 +57,7 @@ export class Rule {
57
57
  if (leftWidth > 0) {
58
58
  yield new Segment(repeatToWidth(ruleChar, leftWidth), ruleStyle);
59
59
  }
60
- yield new Segment(titleText, ruleStyle);
60
+ yield* title;
61
61
  if (rightWidth > 0) {
62
62
  yield new Segment(repeatToWidth(ruleChar, rightWidth), ruleStyle);
63
63
  }
@@ -19,10 +19,12 @@ export declare class Spinner implements Renderable, Measurable {
19
19
  constructor(name?: string, text?: string, options?: SpinnerOptions);
20
20
  get frames(): readonly string[];
21
21
  get interval(): number;
22
- /** Advance frame based on elapsed time and return current frame. */
23
- private _currentFrame;
22
+ /** Advance the frame count by the time elapsed, and return it. */
23
+ private _advance;
24
+ /** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
25
+ private _drawnFrames;
24
26
  render(options: RenderOptions): Iterable<Segment>;
25
- measure(_options: RenderOptions): {
27
+ measure(options: RenderOptions): {
26
28
  minimum: number;
27
29
  maximum: number;
28
30
  };
@@ -4,8 +4,9 @@
4
4
  import { cellLen } from "../core/cells.js";
5
5
  import { Segment } from "../core/segment.js";
6
6
  import { NULL_STYLE } from "../core/style.js";
7
- import { SPINNERS, DEFAULT_SPINNER } from "../core/spinnerData.js";
8
- import { getStyle } from "../core/protocol.js";
7
+ import { SPINNERS, DEFAULT_SPINNER, LINE_SPINNER } from "../core/spinnerData.js";
8
+ import { drawable, getStyle } from "../core/protocol.js";
9
+ const spinnerGlyphs = ({ frames }) => frames.join("");
9
10
  export class Spinner {
10
11
  name;
11
12
  text;
@@ -34,8 +35,8 @@ export class Spinner {
34
35
  get interval() {
35
36
  return this._data.interval;
36
37
  }
37
- /** Advance frame based on elapsed time and return current frame. */
38
- _currentFrame() {
38
+ /** Advance the frame count by the time elapsed, and return it. */
39
+ _advance() {
39
40
  const now = Date.now();
40
41
  const elapsed = now - this._lastUpdate;
41
42
  const effectiveInterval = this.interval / this.speed;
@@ -44,10 +45,15 @@ export class Spinner {
44
45
  this._frameIndex = (this._frameIndex + steps) % this._data.frames.length;
45
46
  this._lastUpdate = now;
46
47
  }
47
- return this._data.frames[this._frameIndex];
48
+ return this._frameIndex;
49
+ }
50
+ /** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
51
+ _drawnFrames(options) {
52
+ return drawable(options, this._data, LINE_SPINNER, spinnerGlyphs).frames;
48
53
  }
49
54
  *render(options) {
50
- const frame = this._currentFrame();
55
+ const frames = this._drawnFrames(options);
56
+ const frame = frames[this._advance() % frames.length];
51
57
  const style = getStyle(options, this.style);
52
58
  const spinStyle = style.isNull ? undefined : style;
53
59
  yield new Segment(frame, spinStyle);
@@ -55,8 +61,8 @@ export class Spinner {
55
61
  yield new Segment(` ${this.text}`);
56
62
  }
57
63
  }
58
- measure(_options) {
59
- const frameWidth = Math.max(...this._data.frames.map((f) => cellLen(f)));
64
+ measure(options) {
65
+ const frameWidth = Math.max(...this._drawnFrames(options).map((f) => cellLen(f)));
60
66
  const textWidth = this.text ? cellLen(this.text) + 1 : 0;
61
67
  const total = frameWidth + textWidth;
62
68
  return { minimum: frameWidth, maximum: total };
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { Segment } from "../core/segment.js";
6
6
  import type { PaddingDimensions } from "./padding.js";
7
- import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ import { type Measurable, type Renderable, type RenderOptions } from "../core/protocol.js";
8
8
  export interface SyntaxOptions {
9
9
  lineNumbers?: boolean;
10
10
  theme?: string;
@@ -6,6 +6,7 @@ import { cellLen } from "../core/cells.js";
6
6
  import { Segment } from "../core/segment.js";
7
7
  import { Style } from "../core/style.js";
8
8
  import { RichText } from "../core/text.js";
9
+ import { drawable } from "../core/protocol.js";
9
10
  // Built-in token patterns for common languages
10
11
  const TOKEN_PATTERNS = {
11
12
  _default: [
@@ -72,7 +73,7 @@ export class Syntax {
72
73
  ? Style.parse("bold on grey27")
73
74
  : Style.parse("dim");
74
75
  yield new Segment(numStr, numStyle);
75
- yield new Segment("│ ", Style.parse("dim"));
76
+ yield new Segment(drawable(options, "│ ", "| "), Style.parse("dim"));
76
77
  }
77
78
  const line = textLines[i];
78
79
  const segs = [...line.render({ ...options, maxWidth: options.maxWidth - lineNumWidth - 2 })];
@@ -6,7 +6,7 @@ import { Style } from "../core/style.js";
6
6
  import { Box } from "../core/box.js";
7
7
  import { RichText } from "../core/text.js";
8
8
  import type { PaddingDimensions } from "./padding.js";
9
- import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
9
+ import type { Renderable, Measurable, OverflowMethod, RenderOptions } from "../core/protocol.js";
10
10
  export interface ColumnOptions {
11
11
  header?: string | RichText;
12
12
  footer?: string | RichText;
@@ -19,7 +19,7 @@ export interface ColumnOptions {
19
19
  maxWidth?: number;
20
20
  ratio?: number;
21
21
  noWrap?: boolean;
22
- overflow?: "fold" | "crop" | "ellipsis";
22
+ overflow?: OverflowMethod;
23
23
  }
24
24
  export declare class Column {
25
25
  private _header;
@@ -33,7 +33,7 @@ export declare class Column {
33
33
  maxWidth: number | undefined;
34
34
  ratio: number | undefined;
35
35
  noWrap: boolean;
36
- overflow: "fold" | "crop" | "ellipsis";
36
+ overflow: OverflowMethod;
37
37
  private _cells;
38
38
  constructor(options?: ColumnOptions);
39
39
  /**
@@ -42,7 +42,7 @@ export declare class Column {
42
42
  * constructor-only stamp held only until the first `columns[0].footer = mine`
43
43
  * — which installed content that had parsed no markup, still carried its
44
44
  * `end`, and was still owned by the caller, into a slot every reader below
45
- * assumes `toCellText` has been through. [LAW:parse-dont-validate] The setter
45
+ * assumes `embeddedText` has been through. [LAW:parse-dont-validate] The setter
46
46
  * is the border, so the guarantee holds for the object's whole lifetime and
47
47
  * the constructor is one caller of it rather than the one place it is true.
48
48
  *
@@ -51,7 +51,7 @@ export declare class Column {
51
51
  * a column always has one and `show_footer` alone decides whether it is
52
52
  * drawn. Modelling the absence as `undefined` instead made "no column has a
53
53
  * footer" a state the render path could ask about — and it did, skipping the
54
- * row a caller had asked for. `toCellText` already maps nothing onto empty,
54
+ * row a caller had asked for. `embeddedText` already maps nothing onto empty,
55
55
  * which is why the setters take `undefined` rather than defaulting around it.
56
56
  */
57
57
  get header(): RichText;
@@ -121,11 +121,13 @@ export declare class Table implements Renderable, Measurable {
121
121
  * [LAW:one-source-of-truth] Both ends of the range are widths the geometry
122
122
  * actually produced — the maximum from the demands as they stand, the
123
123
  * minimum from the same layout with every column asking for a single cell.
124
- * Neither end stretches: a stretch only spends cells an offer happens to
125
- * leave over, and a renderable reports the width its content wants rather
126
- * than the width it was offered. Letting it in made an expanding table
127
- * measure `Infinity` against an unbounded offer, where `withBoundedWidth`
128
- * needs a natural width to fall back on.
124
+ * Neither end stretches or fills into an offer: both only spend cells an
125
+ * offer happens to leave over, and a renderable reports the width its content
126
+ * wants rather than the width it was offered. A declared `width` is not an
127
+ * offer, and the maximum reports it. Letting either in made the table measure
128
+ * `Infinity` against an unbounded offer, where `withBoundedWidth` needs a
129
+ * natural width to fall back on — an expanding table, and an empty
130
+ * `Table.grid()`.
129
131
  * Neither can exceed the width offered and the tighter request cannot exceed
130
132
  * the looser one, so the range cannot invert. Deriving the minimum from raw
131
133
  * column and padding counts instead is what used to return
@@ -172,11 +174,21 @@ export declare class Table implements Renderable, Measurable {
172
174
  */
173
175
  private _columnCells;
174
176
  /**
175
- * The widest cell in a column, bounded by its own `minWidth`/`maxWidth`.
176
- * Zero for a column that draws nothing — a gutter asks for its padding and
177
- * nothing else.
177
+ * The widest cell in a column, or `undefined` when the column has no cell to
178
+ * measure. Zero is a column whose cells draw nothing — a gutter asks for its
179
+ * padding and nothing else — and is not the same answer.
180
+ * [LAW:types-are-the-program]
178
181
  */
179
- private _naturalWidth;
182
+ private _widestCell;
183
+ /**
184
+ * A natural width held to the column's own `minWidth`/`maxWidth`.
185
+ *
186
+ * A bound is a count of cells, read by the rule every width is read by: NaN
187
+ * and a negative are zero cells, so a NaN floor bounds nothing and a NaN
188
+ * ceiling is a ceiling of zero, as `width: NaN` is. A bare `Math.max` returns
189
+ * NaN instead, and the column vanishes. [LAW:one-source-of-truth]
190
+ */
191
+ private _bounded;
180
192
  private _renderRow;
181
193
  private _renderTitle;
182
194
  }