@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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * place — a renderable's output set down as one block inside a wider width.
3
+ *
4
+ * The block is placed, not its lines. The renderable is measured, drawn at the
5
+ * width it asked for and at its own height — a region's height is dropped, as
6
+ * the reference's `Align` drops it — and every line is padded to the widest, so the block is a
7
+ * rectangle; only then is the rectangle padded to the offered width. A
8
+ * multi-line paragraph keeps its own shape under `"center"` — `hi` over `hello`
9
+ * stays a 5-cell block, centred as one — where padding each line to the full
10
+ * width on its own would centre `hi` against the whole terminal. That is
11
+ * Rich's `Align`, and `test/core/console.test.ts` pins it against the
12
+ * reference's bytes.
13
+ *
14
+ * It lives in `core/` because `Console.print` places with it: Rich's `print`
15
+ * wraps what it prints in `Align` for the three alignments, and `console.ts`
16
+ * reaching for `renderables/align.ts` would be a third upward edge out of this
17
+ * layer. [LAW:one-way-deps] `Align` is this function behind the `Renderable`
18
+ * interface. [LAW:one-source-of-truth]
19
+ */
20
+ import { Segment } from "./segment.js";
21
+ import { Measurement } from "./measure.js";
22
+ import { isMeasurable } from "./protocol.js";
23
+ // The share of the spare cells that goes on the left; the rest go on the right.
24
+ // Right-alignment pads no right side at all, which is the reference's choice
25
+ // and not a zero: a line that ends at the edge carries no trailing blanks.
26
+ const PADS = {
27
+ left: (spare) => [0, spare],
28
+ center: (spare) => [Math.floor(spare / 2), spare - Math.floor(spare / 2)],
29
+ right: (spare) => [spare, 0],
30
+ };
31
+ /**
32
+ * `renderable`'s lines, the block they form placed by `align` in
33
+ * `options.maxWidth`. `options.maxWidth` must be finite: the spare cells are
34
+ * written out as spaces. A block wider than the offer — a line with nowhere to
35
+ * break, drawn by a renderable that does not cut its own — is left unpadded at
36
+ * its full width, for whatever holds it to crop or keep.
37
+ */
38
+ export function placeBlock(renderable, align, options) {
39
+ // A renderable that cannot say how wide it wants to be is offered all of it,
40
+ // and the block is then only as wide as what it actually drew.
41
+ const width = isMeasurable(renderable)
42
+ ? Measurement.get(options, renderable).maximum
43
+ : options.maxWidth;
44
+ const drawn = Segment.splitLines(renderable.render({ ...options, maxWidth: width, height: undefined }));
45
+ const [blockWidth, height] = Segment.getShape(drawn);
46
+ const [left, right] = PADS[align](Math.max(0, options.maxWidth - blockWidth));
47
+ return Segment.setShape(drawn, blockWidth, height).map((line) => [...pad(left), ...line, ...pad(right)]);
48
+ }
49
+ // [LAW:dataflow-not-control-flow] no spare room is a pad of no segments, not a
50
+ // step skipped.
51
+ const pad = (cells) => (cells > 0 ? [new Segment(" ".repeat(cells))] : []);
@@ -17,6 +17,7 @@
17
17
  * times.
18
18
  */
19
19
  import { Segment } from "./segment.js";
20
+ import { RichText } from "./text.js";
20
21
  import type { Highlighter } from "./highlighter.js";
21
22
  import type { Renderable, Measurable, RenderOptions } from "./protocol.js";
22
23
  export interface PrettyOptions {
@@ -56,6 +57,8 @@ export declare class Pretty implements Renderable, Measurable {
56
57
  readonly highlighter: Highlighter;
57
58
  constructor(data: unknown, options?: PrettyOptions);
58
59
  render(options: RenderOptions): Iterable<Segment>;
60
+ /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
61
+ toText(options: RenderOptions): RichText;
59
62
  measure(_options: RenderOptions): {
60
63
  minimum: number;
61
64
  maximum: number;
@@ -163,13 +163,17 @@ export class Pretty {
163
163
  this.highlighter = options?.highlighter ?? reprHighlighter;
164
164
  }
165
165
  *render(options) {
166
+ yield* this.toText(options).render(options);
167
+ }
168
+ /** The value laid out for `options.maxWidth` and highlighted: the text this renders. */
169
+ toText(options) {
166
170
  const formatted = this._format(this.data, rootFrame(options.maxWidth));
167
171
  const text = new RichText(formatted, { end: "" });
168
172
  this.highlighter.highlight(text);
169
173
  if (this.indentGuides) {
170
174
  this._addIndentGuides(text);
171
175
  }
172
- yield* text.render(options);
176
+ return text;
173
177
  }
174
178
  measure(_options) {
175
179
  const formatted = this._format(this.data, rootFrame(_options.maxWidth));
@@ -5,6 +5,17 @@
5
5
  import type { Segment } from "./segment.js";
6
6
  import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
7
7
  import type { ColorDepth } from "./color.js";
8
+ /**
9
+ * What becomes of a line still wider than its width once wrapping is done.
10
+ * `"fold"` breaks it across lines, `"crop"` cuts it at the edge, `"ellipsis"`
11
+ * cuts it and marks the cut. `"ignore"` is not a way of cutting: the line has
12
+ * no edge, so nothing wraps it and nothing cuts it, and it leaves at its
13
+ * natural width for whatever drew it to crop or not.
14
+ *
15
+ * `noWrap` is the other half of Rich's pair and is not `"ignore"`: it stops the
16
+ * wrapping and leaves the edge, so a line too wide is cut by the method here.
17
+ */
18
+ export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
8
19
  export interface RenderOptions {
9
20
  /**
10
21
  * The cells this renderable may occupy. A count of cells, so a non-negative
@@ -15,14 +26,20 @@ export interface RenderOptions {
15
26
  */
16
27
  maxWidth: number;
17
28
  minWidth?: number;
18
- height?: number;
19
- maxHeight?: number;
29
+ /**
30
+ * The rows this renderable may occupy. Absent, nothing limits them.
31
+ */
32
+ height?: Height;
20
33
  isTerminal?: boolean;
21
- encoding?: string;
22
- legacyWindows?: boolean;
34
+ /**
35
+ * The output can draw only ASCII. Every glyph a renderable chooses — box
36
+ * lines, tree guides, a check mark — is resolved through `drawable`. Text
37
+ * the caller hands in is drawn as given. Absent means the output draws any
38
+ * glyph.
39
+ */
23
40
  asciiOnly?: boolean;
24
41
  justify?: "left" | "center" | "right" | "full";
25
- overflow?: "fold" | "crop" | "ellipsis";
42
+ overflow?: OverflowMethod;
26
43
  noWrap?: boolean;
27
44
  highlight?: unknown;
28
45
  markup?: unknown;
@@ -46,7 +63,95 @@ export interface RenderOptions {
46
63
  * the render strict: the error leaves `render`.
47
64
  */
48
65
  onStyleError?: StyleErrorHandler;
66
+ /**
67
+ * Told of each owner that stamps its output (`core/anchor`) as it starts to
68
+ * render, so the calls arrive in document order: a container's children in
69
+ * the order it renders them, an owner before the owners nested in it. A
70
+ * container forwards it by passing its options on, which every container
71
+ * does. An owner rendered twice is told twice. A render made to measure is
72
+ * not drawing and is not told (`measuring`).
73
+ */
74
+ onDraw?: (owner: object) => void;
75
+ }
76
+ /**
77
+ * `chosen` as this output can draw it: `ascii` in its place when the output is
78
+ * ASCII-only and `chosen` spends a glyph outside ASCII, otherwise `chosen`
79
+ * itself. Choosing a glyph that is already ASCII (a rule of `=`, a MARKDOWN box)
80
+ * picks a frame the terminal can draw, so it is kept. A set of glyphs that must
81
+ * change together — a box grid, a spinner's frames, a scrollbar — passes
82
+ * `glyphs` to say which characters it spends, and is replaced whole.
83
+ *
84
+ * [LAW:single-enforcer] The one place that decides what `asciiOnly` replaces.
85
+ * `test/renderables/ascii-only.test.ts` renders every exported renderable to
86
+ * hold each of them to it.
87
+ */
88
+ export declare function drawable(options: {
89
+ readonly asciiOnly?: boolean;
90
+ }, chosen: string, ascii: string): string;
91
+ export declare function drawable<T>(options: {
92
+ readonly asciiOnly?: boolean;
93
+ }, chosen: T, ascii: T, glyphs: (value: T) => string): T;
94
+ /**
95
+ * A vertical budget: a count of rows, and whether they are a region the
96
+ * renderable stands in or a ceiling it stays under.
97
+ *
98
+ * `exact: true` is a region — a `Layout` pane, a full-screen frame. The output
99
+ * is exactly `rows` tall. A renderable may fill the region — a layout divides
100
+ * it, a frame can stretch to its last row — or ignore it and emit its natural
101
+ * height; either way, whoever set the region shapes what comes back to it
102
+ * (`fitHeight`). Setting a region is a promise to shape, so no renderable pads
103
+ * or crops itself to one.
104
+ *
105
+ * `exact: false` is a ceiling — the terminal an inline print lands on. Content
106
+ * keeps its natural height beneath it, and nothing pads up to the ceiling.
107
+ * Output taller than the ceiling is its setter's to handle: an inline `Live`
108
+ * applies its `verticalOverflow`, and a print lets the terminal scroll it.
109
+ *
110
+ * A renderable passing its whole space to one child must forward the budget
111
+ * less the rows it draws itself, and of the same kind (`insetHeight`). One
112
+ * stacking several children must hand each the rows as a ceiling
113
+ * (`stackedHeight`): any one of them may use all of it, and none may claim it
114
+ * as its region. Forwarded unchanged, a layout nested in a panel fills the
115
+ * panel's whole region and the region's crop takes the panel's bottom border.
116
+ *
117
+ * [LAW:types-are-the-program] One field, because the fact is one count and one
118
+ * bit about it. As two numbers, `height` and `maxHeight`, it admitted a region
119
+ * taller than its ceiling and a region with no ceiling at all, and the one
120
+ * reader in the library resolved them `maxHeight ?? height` — the ceiling ahead
121
+ * of the region it contains.
122
+ *
123
+ * There is no vertical `Measurable`. A parent chooses widths from its
124
+ * children's measurements before any of them renders; nothing chooses rows
125
+ * that way, because a renderable's height is a consequence of the width it
126
+ * renders at. Rendering and counting the lines is the measurement.
127
+ */
128
+ export interface Height {
129
+ readonly rows: number;
130
+ readonly exact: boolean;
49
131
  }
132
+ /**
133
+ * The budget a renderable hands the one child filling its space: its own, less
134
+ * the `rows` it draws itself, and of the same kind.
135
+ */
136
+ export declare function insetHeight(height: Height | undefined, rows: number): Height | undefined;
137
+ /**
138
+ * The budget a renderable hands each of several children it stacks: its rows,
139
+ * as a ceiling.
140
+ */
141
+ export declare function stackedHeight(height: Height | undefined): Height | undefined;
142
+ /**
143
+ * The rows a region holds, parsed as a cell count, or `undefined` when there
144
+ * is no region to fill: a ceiling, no budget, or a region of `Infinity` rows,
145
+ * which names no count — as an unbounded width resolves to a natural one in
146
+ * `withBoundedWidth`.
147
+ */
148
+ export declare function regionRows(height: Height | undefined): number | undefined;
149
+ /**
150
+ * What came back from a child, held to the region `height` names — blank rows
151
+ * padded below, overflow cropped from the bottom — or left at its own height
152
+ * when `height` is no region. This is the shaping a region's setter owes.
153
+ */
154
+ export declare function fitHeight(lines: Segment[][], height: Height | undefined): Segment[][];
50
155
  /**
51
156
  * Receives a style error a render would otherwise absorb: the parse failure,
52
157
  * and the whole style string it came from — the error names only the token
@@ -58,7 +163,12 @@ export interface RenderOptions {
58
163
  */
59
164
  export type StyleErrorHandler = (error: StyleSyntaxError, style: string) => void;
60
165
  /**
61
- * The style a `string | Style` stands for in this render.
166
+ * The style a `string | Style` stands for in this render: a name from the
167
+ * theme of whatever is drawing, otherwise a style definition. A string that is
168
+ * neither throws `StyleSyntaxError`. This is how a renderable that yields its
169
+ * own `Segment`s draws with a theme name, since a `Segment` takes a resolved
170
+ * `Style`. It is Rich's `console.get_style`, asked of the options because the
171
+ * options are where the theme travels.
62
172
  *
63
173
  * [LAW:single-enforcer] Every renderable resolves a style name here, at render
64
174
  * time, because only the render knows whose theme it is drawing for. Resolved
@@ -123,6 +233,16 @@ export declare function withCellWidth(options: RenderOptions): RenderOptions;
123
233
  * rather than zero, and saying so names the cause — where `String.repeat` and
124
234
  * `new Array` only ever name their own argument.
125
235
  */
236
+ /**
237
+ * `options` as a measurement is asked with. A `measure` that renders to learn
238
+ * its shape draws nothing on the frame, so it reports no owner to `onDraw` —
239
+ * one that did would put a widget in document order at the moment a
240
+ * container measured it, and keep one the frame then cropped away.
241
+ *
242
+ * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
243
+ * `withBoundedWidth` ask through it, and each `measure` passes it on.
244
+ */
245
+ export declare function measuring(options: RenderOptions): RenderOptions;
126
246
  export declare function withBoundedWidth(options: RenderOptions, self: Measurable): RenderOptions;
127
247
  export interface Renderable {
128
248
  render(options: RenderOptions): Iterable<Segment>;
@@ -133,5 +253,15 @@ export interface Measurable {
133
253
  maximum: number;
134
254
  };
135
255
  }
256
+ /**
257
+ * What the wheel scrolls: an owner of anchors on the frame (`./anchor.ts`)
258
+ * that moves by lines. `canScrollBy` says whether `scrollBy` with the same
259
+ * `lines` would move it; one that would not passes the wheel outward.
260
+ */
261
+ export interface Scrollable {
262
+ canScrollBy(lines: number): boolean;
263
+ scrollBy(lines: number): void;
264
+ }
136
265
  export declare function isRenderable(obj: unknown): obj is Renderable;
137
266
  export declare function isMeasurable(obj: unknown): obj is Measurable;
267
+ export declare function isScrollable(obj: object): obj is Scrollable;
@@ -4,8 +4,52 @@
4
4
  */
5
5
  import { cellCount } from "./cells.js";
6
6
  import { DEFAULT_THEME } from "./style.js";
7
+ const ASCII_TEXT = /^[\x00-\x7F]*$/;
8
+ export function drawable(options, chosen, ascii, glyphs = String) {
9
+ return options.asciiOnly === true && !ASCII_TEXT.test(glyphs(chosen)) ? ascii : chosen;
10
+ }
11
+ /**
12
+ * The budget a renderable hands the one child filling its space: its own, less
13
+ * the `rows` it draws itself, and of the same kind.
14
+ */
15
+ export function insetHeight(height, rows) {
16
+ return height && { rows: cellCount(height.rows - rows), exact: height.exact };
17
+ }
18
+ /**
19
+ * The budget a renderable hands each of several children it stacks: its rows,
20
+ * as a ceiling.
21
+ */
22
+ export function stackedHeight(height) {
23
+ return height && { rows: cellCount(height.rows), exact: false };
24
+ }
25
+ /**
26
+ * The rows a region holds, parsed as a cell count, or `undefined` when there
27
+ * is no region to fill: a ceiling, no budget, or a region of `Infinity` rows,
28
+ * which names no count — as an unbounded width resolves to a natural one in
29
+ * `withBoundedWidth`.
30
+ */
31
+ export function regionRows(height) {
32
+ return height?.exact && height.rows !== Infinity ? cellCount(height.rows) : undefined;
33
+ }
7
34
  /**
8
- * The style a `string | Style` stands for in this render.
35
+ * What came back from a child, held to the region `height` names — blank rows
36
+ * padded below, overflow cropped from the bottom — or left at its own height
37
+ * when `height` is no region. This is the shaping a region's setter owes.
38
+ */
39
+ export function fitHeight(lines, height) {
40
+ const rows = regionRows(height) ?? lines.length;
41
+ const fitted = lines.slice(0, rows);
42
+ while (fitted.length < rows)
43
+ fitted.push([]);
44
+ return fitted;
45
+ }
46
+ /**
47
+ * The style a `string | Style` stands for in this render: a name from the
48
+ * theme of whatever is drawing, otherwise a style definition. A string that is
49
+ * neither throws `StyleSyntaxError`. This is how a renderable that yields its
50
+ * own `Segment`s draws with a theme name, since a `Segment` takes a resolved
51
+ * `Style`. It is Rich's `console.get_style`, asked of the options because the
52
+ * options are where the theme travels.
9
53
  *
10
54
  * [LAW:single-enforcer] Every renderable resolves a style name here, at render
11
55
  * time, because only the render knows whose theme it is drawing for. Resolved
@@ -74,11 +118,23 @@ export function withCellWidth(options) {
74
118
  * rather than zero, and saying so names the cause — where `String.repeat` and
75
119
  * `new Array` only ever name their own argument.
76
120
  */
121
+ /**
122
+ * `options` as a measurement is asked with. A `measure` that renders to learn
123
+ * its shape draws nothing on the frame, so it reports no owner to `onDraw` —
124
+ * one that did would put a widget in document order at the moment a
125
+ * container measured it, and keep one the frame then cropped away.
126
+ *
127
+ * [LAW:single-enforcer] Every measurement starts here: `Measurement.get` and
128
+ * `withBoundedWidth` ask through it, and each `measure` passes it on.
129
+ */
130
+ export function measuring(options) {
131
+ return { ...options, onDraw: undefined };
132
+ }
77
133
  export function withBoundedWidth(options, self) {
78
134
  const parsed = withCellWidth(options);
79
135
  if (Number.isFinite(parsed.maxWidth))
80
136
  return parsed;
81
- const natural = self.measure(parsed).maximum;
137
+ const natural = self.measure(measuring(parsed)).maximum;
82
138
  if (!Number.isFinite(natural)) {
83
139
  throw new RangeError("maxWidth is unbounded and this renderable has no natural width to fall " +
84
140
  "back on: its content does not implement measure(), or something inside " +
@@ -99,3 +155,9 @@ export function isMeasurable(obj) {
99
155
  "measure" in obj &&
100
156
  typeof obj.measure === "function");
101
157
  }
158
+ export function isScrollable(obj) {
159
+ return ("canScrollBy" in obj &&
160
+ typeof obj.canScrollBy === "function" &&
161
+ "scrollBy" in obj &&
162
+ typeof obj.scrollBy === "function");
163
+ }
@@ -38,8 +38,8 @@
38
38
  import { ColorDepth } from "./color.js";
39
39
  import type { Destination } from "./color.js";
40
40
  import type { Segment } from "./segment.js";
41
- import type { Renderable } from "./protocol.js";
42
- export interface RenderToStringOptions {
41
+ import type { Renderable, RenderOptions } from "./protocol.js";
42
+ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "onStyleError"> {
43
43
  /** Cell width to render into. Default 80. */
44
44
  width?: number;
45
45
  /**
@@ -135,8 +135,8 @@ export function renderToString(renderable, options) {
135
135
  const renderOptions = {
136
136
  maxWidth: width,
137
137
  isTerminal: false,
138
- encoding: "utf-8",
139
- asciiOnly: false,
138
+ asciiOnly: options?.asciiOnly ?? false,
139
+ onStyleError: options?.onStyleError,
140
140
  // [LAW:one-source-of-truth] The depth the segments below are encoded at,
141
141
  // so a renderable measures what this very call will draw.
142
142
  colorSystem: destination.colorSystem,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { type CellCol } from "./cells.js";
6
6
  import { Style } from "./style.js";
7
+ import { type Anchor } from "./anchor.js";
7
8
  export declare enum ControlType {
8
9
  BELL = "bell",
9
10
  CARRIAGE_RETURN = "carriage_return",
@@ -33,10 +34,27 @@ export declare class Segment {
33
34
  get isControl(): boolean;
34
35
  /**
35
36
  * Splits at a cell position. Returns [left, right].
37
+ *
38
+ * [LAW:single-enforcer] The right half starts as many cells further into
39
+ * whatever drew it as the left half took from this one, and its anchor says
40
+ * so; see `./anchor.ts`. That is not `leftText`'s width: a cut through a
41
+ * wide glyph pads the left half and keeps the whole glyph on the right.
36
42
  */
37
43
  splitCells(position: CellCol): [Segment, Segment];
38
44
  private static _line;
39
45
  static line(): Segment;
46
+ /**
47
+ * `lines`, as drawn by `owner`: every segment stamped with the row and
48
+ * column of its first cell, wrapping whatever anchor it already carried.
49
+ * Control segments occupy no cell and are left as they are. `firstRow` is
50
+ * the owner's row that `lines[0]` is, for output that arrives in pieces.
51
+ */
52
+ static anchorLines(lines: Segment[][], owner: object, firstRow?: number): Segment[][];
53
+ /**
54
+ * The anchor of the cell at column `x` of row `y` in a composed frame, or
55
+ * `undefined` when that cell was drawn by no owner or lies outside the frame.
56
+ */
57
+ static anchorAt(lines: readonly (readonly Segment[])[], x: number, y: number): Anchor | undefined;
40
58
  /**
41
59
  * Yields segments with combined styles.
42
60
  */
@@ -79,6 +97,11 @@ export declare class Segment {
79
97
  * nothing, and a segment the crop did not change leaves as itself.
80
98
  */
81
99
  static cropLines(segments: Iterable<Segment>, width: number): Iterable<Segment>;
100
+ /**
101
+ * Splits a renderable's output into rows, each exactly `width` cells:
102
+ * short rows padded with spaces, long ones cut.
103
+ */
104
+ static splitAndCropLines(segments: Iterable<Segment>, width: number): Segment[][];
82
105
  /**
83
106
  * Returns total cell width of a line. Ignores control segments.
84
107
  */
@@ -135,8 +158,4 @@ export declare class Segment {
135
158
  * Forces lines to exactly width x height.
136
159
  */
137
160
  static setShape(lines: Segment[][], width: number, height: number, style?: Style): Segment[][];
138
- /**
139
- * Splits segments into lines and adjusts each to exactly `width` cells.
140
- */
141
- static splitAndCropLines(segments: Iterable<Segment>, width: number, pad?: boolean, includeNewLines?: boolean, style?: Style): Segment[][];
142
161
  }
@@ -3,6 +3,8 @@
3
3
  * is represented as a Segment: (text, style?, control?).
4
4
  */
5
5
  import { cellLen, splitText, asCellCol } from "./cells.js";
6
+ import { Style } from "./style.js";
7
+ import { shiftAnchor } from "./anchor.js";
6
8
  // --- ControlType ---
7
9
  export var ControlType;
8
10
  (function (ControlType) {
@@ -44,6 +46,11 @@ export class Segment {
44
46
  }
45
47
  /**
46
48
  * Splits at a cell position. Returns [left, right].
49
+ *
50
+ * [LAW:single-enforcer] The right half starts as many cells further into
51
+ * whatever drew it as the left half took from this one, and its anchor says
52
+ * so; see `./anchor.ts`. That is not `leftText`'s width: a cut through a
53
+ * wide glyph pads the left half and keeps the whole glyph on the right.
47
54
  */
48
55
  splitCells(position) {
49
56
  const len = this.cellLength;
@@ -54,7 +61,7 @@ export class Segment {
54
61
  const [leftText, rightText] = splitText(this.text, position);
55
62
  return [
56
63
  new Segment(leftText, this.style),
57
- new Segment(rightText, this.style),
64
+ new Segment(rightText, this.style?.shiftedBy(len - cellLen(rightText))),
58
65
  ];
59
66
  }
60
67
  // --- Static factories ---
@@ -63,10 +70,54 @@ export class Segment {
63
70
  return (Segment._line ??= new Segment("\n"));
64
71
  }
65
72
  // --- Static transformations ---
73
+ /**
74
+ * `lines`, as drawn by `owner`: every segment stamped with the row and
75
+ * column of its first cell, wrapping whatever anchor it already carried.
76
+ * Control segments occupy no cell and are left as they are. `firstRow` is
77
+ * the owner's row that `lines[0]` is, for output that arrives in pieces.
78
+ */
79
+ static anchorLines(lines, owner, firstRow = 0) {
80
+ return lines.map((line, i) => {
81
+ const row = firstRow + i;
82
+ let col = 0;
83
+ return line.map((segment) => {
84
+ if (segment.isControl)
85
+ return segment;
86
+ const style = segment.style ?? Style.null();
87
+ const anchored = new Segment(segment.text, style.withAnchor({ owner, row, col, inner: style.anchor }));
88
+ col += segment.cellLength;
89
+ return anchored;
90
+ });
91
+ });
92
+ }
93
+ /**
94
+ * The anchor of the cell at column `x` of row `y` in a composed frame, or
95
+ * `undefined` when that cell was drawn by no owner or lies outside the frame.
96
+ */
97
+ static anchorAt(lines, x, y) {
98
+ if (!Number.isInteger(x) || x < 0)
99
+ return undefined;
100
+ let start = 0;
101
+ for (const segment of lines[y] ?? []) {
102
+ const end = start + segment.cellLength;
103
+ if (x < end) {
104
+ const anchor = segment.style?.anchor;
105
+ return anchor && shiftAnchor(anchor, x - start);
106
+ }
107
+ start = end;
108
+ }
109
+ return undefined;
110
+ }
66
111
  /**
67
112
  * Yields segments with combined styles.
68
113
  */
69
114
  static *applyStyle(segments, style, postStyle) {
115
+ // Applying no style is the identity, so the segments pass through as they
116
+ // are instead of each being copied into an equal one.
117
+ if (!style && !postStyle) {
118
+ yield* segments;
119
+ return;
120
+ }
70
121
  for (const segment of segments) {
71
122
  if (segment.isControl) {
72
123
  yield segment;
@@ -156,6 +207,22 @@ export class Segment {
156
207
  }
157
208
  return result;
158
209
  }
210
+ // --- Bounding output to a width ---
211
+ //
212
+ // Two primitives, because containers ask two different questions of content.
213
+ //
214
+ // `cropLines` — *keep the structure, bound it*. The container needs no line
215
+ // to overrun and must not pad: a `Layout` leaf and the console's crop pass
216
+ // the stream on as it is; a `Tree` label splits the cropped stream itself to
217
+ // hang its guides on each row. The crop never decides how many lines there
218
+ // are — whoever splits after it owns that.
219
+ //
220
+ // `splitAndCropLines` — *decompose into rows of exactly this width*. The
221
+ // container goes on to compose the rows (a `Table` cell zipped beside its
222
+ // neighbours), so it needs the two-dimensional shape and every row full.
223
+ //
224
+ // The split decides how many lines there are, so it is wrong for the first
225
+ // question; `cropLines`' header records the two times it was tried there.
159
226
  /**
160
227
  * Crops a renderable's output so no line exceeds `width`, leaving short lines
161
228
  * alone and the line structure exactly as it arrived.
@@ -201,6 +268,13 @@ export class Segment {
201
268
  yield cropped === segment.text ? segment : new Segment(cropped, segment.style);
202
269
  }
203
270
  }
271
+ /**
272
+ * Splits a renderable's output into rows, each exactly `width` cells:
273
+ * short rows padded with spaces, long ones cut.
274
+ */
275
+ static splitAndCropLines(segments, width) {
276
+ return Segment.splitLines(segments).map((line) => Segment.adjustLineLength(line, width));
277
+ }
204
278
  /**
205
279
  * Returns total cell width of a line. Ignores control segments.
206
280
  */
@@ -407,21 +481,6 @@ export class Segment {
407
481
  }
408
482
  return result.slice(0, height);
409
483
  }
410
- /**
411
- * Splits segments into lines and adjusts each to exactly `width` cells.
412
- */
413
- static splitAndCropLines(segments, width, pad = true, includeNewLines = false, style) {
414
- const rawLines = Segment.splitLines(segments);
415
- const result = [];
416
- for (const line of rawLines) {
417
- const adjusted = Segment.adjustLineLength(line, width, style, pad);
418
- result.push(adjusted);
419
- if (includeNewLines) {
420
- result[result.length - 1].push(Segment.line());
421
- }
422
- }
423
- return result;
424
- }
425
484
  }
426
485
  // --- Internal helpers ---
427
486
  function stylesEqual(a, b) {
@@ -6,5 +6,7 @@ export interface SpinnerData {
6
6
  readonly frames: readonly string[];
7
7
  readonly interval: number;
8
8
  }
9
+ /** The spinner an ASCII-only output draws in place of any other. */
10
+ export declare const LINE_SPINNER: SpinnerData;
9
11
  export declare const SPINNERS: Record<string, SpinnerData>;
10
12
  export declare const DEFAULT_SPINNER = "dots";
@@ -2,12 +2,14 @@
2
2
  * Built-in spinner frame data.
3
3
  * [LAW:one-type-per-behavior] All spinners are instances of SpinnerData — differ only by frame data.
4
4
  */
5
+ /** The spinner an ASCII-only output draws in place of any other. */
6
+ export const LINE_SPINNER = { frames: ["-", "\\", "|", "/"], interval: 130 };
5
7
  // [LAW:one-source-of-truth] Single authoritative collection of spinner definitions
6
8
  export const SPINNERS = {
7
9
  dots: { frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], interval: 80 },
8
10
  dots2: { frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], interval: 80 },
9
11
  dots3: { frames: ["⠋", "⠙", "⠚", "⠞", "⠖", "⠦", "⠴", "⠲", "⠳", "⠓"], interval: 80 },
10
- line: { frames: ["-", "\\", "|", "/"], interval: 130 },
12
+ line: LINE_SPINNER,
11
13
  line2: { frames: ["⠂", "-", "–", "—", "–", "-"], interval: 100 },
12
14
  pipe: { frames: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"], interval: 100 },
13
15
  simpleDots: { frames: [". ", ".. ", "...", " "], interval: 400 },
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { Segment } from "./segment.js";
29
29
  import { Style } from "./style.js";
30
- import type { Renderable, RenderOptions } from "./protocol.js";
30
+ import { type Renderable, type RenderOptions } from "./protocol.js";
31
31
  /**
32
32
  * Items in a `Strip` expose their per-edge style so joiners can paint the
33
33
  * transition glyph at each boundary.
@@ -101,10 +101,7 @@ export interface PowerlineJoinerOptions {
101
101
  */
102
102
  export declare const POWERLINE_JOINER_GLYPHS: Readonly<PowerlineJoinerOptions>;
103
103
  export declare class PowerlineJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
104
- private readonly _glyph;
105
- private readonly _divider;
106
- private readonly _lead;
107
- private readonly _tail;
104
+ private readonly _glyphs;
108
105
  constructor(options?: PowerlineJoinerOptions);
109
106
  join(left: T | null, right: T | null): Renderable;
110
107
  }
@@ -117,8 +114,7 @@ export interface CapsuleJoinerOptions {
117
114
  separator?: string;
118
115
  }
119
116
  export declare class CapsuleJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
120
- private readonly _left;
121
- private readonly _right;
117
+ private readonly _caps;
122
118
  private readonly _separator;
123
119
  constructor(options?: CapsuleJoinerOptions);
124
120
  _emit(left: T | null, right: T | null, options: RenderOptions): Iterable<Segment>;