@promptctl/rich-js 0.18.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 (221) hide show
  1. package/dist/core/anchor.d.ts +45 -0
  2. package/dist/core/anchor.js +39 -0
  3. package/dist/core/ansi.d.ts +65 -0
  4. package/dist/core/ansi.js +231 -0
  5. package/dist/core/box.d.ts +143 -0
  6. package/dist/core/box.js +379 -0
  7. package/dist/core/cells.d.ts +159 -0
  8. package/dist/core/cells.js +287 -0
  9. package/dist/core/color.d.ts +246 -0
  10. package/dist/core/color.js +1038 -0
  11. package/dist/core/console.d.ts +165 -0
  12. package/dist/core/console.js +527 -0
  13. package/dist/core/emoji.d.ts +24 -0
  14. package/dist/core/emoji.js +391 -0
  15. package/dist/core/export-html.d.ts +59 -0
  16. package/dist/core/export-html.js +200 -0
  17. package/dist/core/export-lines.d.ts +121 -0
  18. package/dist/core/export-lines.js +141 -0
  19. package/dist/core/highlighter.d.ts +33 -0
  20. package/dist/core/highlighter.js +124 -0
  21. package/dist/core/json.d.ts +27 -0
  22. package/dist/core/json.js +53 -0
  23. package/dist/core/markup.d.ts +119 -0
  24. package/dist/core/markup.js +731 -0
  25. package/dist/core/measure.d.ts +27 -0
  26. package/dist/core/measure.js +62 -0
  27. package/dist/core/oklch.d.ts +161 -0
  28. package/dist/core/oklch.js +312 -0
  29. package/dist/core/osc8.d.ts +68 -0
  30. package/dist/core/osc8.js +90 -0
  31. package/dist/core/place.d.ts +30 -0
  32. package/dist/core/place.js +51 -0
  33. package/dist/core/pretty.d.ts +144 -0
  34. package/dist/core/pretty.js +476 -0
  35. package/dist/core/protocol.d.ts +267 -0
  36. package/dist/core/protocol.js +163 -0
  37. package/dist/core/render.d.ts +84 -0
  38. package/dist/core/render.js +145 -0
  39. package/dist/core/segment.d.ts +161 -0
  40. package/dist/core/segment.js +492 -0
  41. package/dist/core/spinnerData.d.ts +12 -0
  42. package/dist/core/spinnerData.js +117 -0
  43. package/dist/core/strip.d.ts +141 -0
  44. package/dist/core/strip.js +305 -0
  45. package/dist/core/style.d.ts +169 -0
  46. package/dist/core/style.js +695 -0
  47. package/dist/core/subscription.d.ts +12 -0
  48. package/dist/core/subscription.js +12 -0
  49. package/dist/core/text.d.ts +256 -0
  50. package/dist/core/text.js +976 -0
  51. package/dist/core/wrap.d.ts +41 -0
  52. package/dist/core/wrap.js +127 -0
  53. package/dist/host/app.d.ts +121 -0
  54. package/dist/host/app.js +255 -0
  55. package/dist/host/host-environment.d.ts +26 -0
  56. package/dist/host/host-environment.js +44 -0
  57. package/dist/host/index.d.ts +5 -0
  58. package/dist/host/index.js +3 -0
  59. package/dist/host/terminal-host.d.ts +179 -0
  60. package/dist/host/terminal-host.js +186 -0
  61. package/dist/index.d.ts +91 -0
  62. package/dist/index.js +107 -0
  63. package/dist/node/prompt.d.ts +21 -0
  64. package/dist/node/prompt.js +30 -0
  65. package/dist/node/save.d.ts +17 -0
  66. package/dist/node/save.js +21 -0
  67. package/dist/node/terminal-host.d.ts +113 -0
  68. package/dist/node/terminal-host.js +227 -0
  69. package/dist/node/traceback.d.ts +37 -0
  70. package/dist/node/traceback.js +134 -0
  71. package/dist/renderables/align.d.ts +16 -0
  72. package/dist/renderables/align.js +33 -0
  73. package/dist/renderables/columns.d.ts +61 -0
  74. package/dist/renderables/columns.js +152 -0
  75. package/dist/renderables/constrain.d.ts +15 -0
  76. package/dist/renderables/constrain.js +37 -0
  77. package/dist/renderables/embed.d.ts +50 -0
  78. package/dist/renderables/embed.js +71 -0
  79. package/dist/renderables/flexStrip.d.ts +41 -0
  80. package/dist/renderables/flexStrip.js +200 -0
  81. package/dist/renderables/group.d.ts +11 -0
  82. package/dist/renderables/group.js +17 -0
  83. package/dist/renderables/layout.d.ts +92 -0
  84. package/dist/renderables/layout.js +312 -0
  85. package/dist/renderables/live.d.ts +50 -0
  86. package/dist/renderables/live.js +175 -0
  87. package/dist/renderables/markdown.d.ts +24 -0
  88. package/dist/renderables/markdown.js +184 -0
  89. package/dist/renderables/padding.d.ts +38 -0
  90. package/dist/renderables/padding.js +141 -0
  91. package/dist/renderables/panel.d.ts +116 -0
  92. package/dist/renderables/panel.js +307 -0
  93. package/dist/renderables/progress.d.ts +90 -0
  94. package/dist/renderables/progress.js +238 -0
  95. package/dist/renderables/progressBar.d.ts +31 -0
  96. package/dist/renderables/progressBar.js +53 -0
  97. package/dist/renderables/prompt.d.ts +49 -0
  98. package/dist/renderables/prompt.js +125 -0
  99. package/dist/renderables/rule.d.ts +25 -0
  100. package/dist/renderables/rule.js +90 -0
  101. package/dist/renderables/spinner.d.ts +31 -0
  102. package/dist/renderables/spinner.js +70 -0
  103. package/dist/renderables/status.d.ts +23 -0
  104. package/dist/renderables/status.js +60 -0
  105. package/dist/renderables/syntax.d.ts +35 -0
  106. package/dist/renderables/syntax.js +115 -0
  107. package/dist/renderables/table.d.ts +194 -0
  108. package/dist/renderables/table.js +795 -0
  109. package/dist/renderables/traceback.d.ts +31 -0
  110. package/dist/renderables/traceback.js +108 -0
  111. package/dist/renderables/tree.d.ts +68 -0
  112. package/dist/renderables/tree.js +150 -0
  113. package/dist/renderables/viewport.d.ts +114 -0
  114. package/dist/renderables/viewport.js +198 -0
  115. package/dist/template-bindings/color-funcs.d.ts +87 -0
  116. package/dist/template-bindings/color-funcs.js +217 -0
  117. package/dist/template-bindings/helpers.d.ts +24 -0
  118. package/dist/template-bindings/helpers.js +48 -0
  119. package/dist/template-bindings/index.d.ts +107 -0
  120. package/dist/template-bindings/index.js +146 -0
  121. package/dist/template-bindings/palette-funcs.d.ts +94 -0
  122. package/dist/template-bindings/palette-funcs.js +141 -0
  123. package/dist/template-bindings/style-funcs.d.ts +28 -0
  124. package/dist/template-bindings/style-funcs.js +155 -0
  125. package/dist/themes/buildPalette.d.ts +36 -0
  126. package/dist/themes/buildPalette.js +56 -0
  127. package/dist/themes/colorMath.d.ts +82 -0
  128. package/dist/themes/colorMath.js +254 -0
  129. package/dist/themes/colorRef.d.ts +51 -0
  130. package/dist/themes/colorRef.js +96 -0
  131. package/dist/themes/data/ansi-tables.d.ts +14 -0
  132. package/dist/themes/data/ansi-tables.js +49 -0
  133. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  134. package/dist/themes/data/atom-one-dark.js +177 -0
  135. package/dist/themes/data/atom-one-light.d.ts +3 -0
  136. package/dist/themes/data/atom-one-light.js +177 -0
  137. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  138. package/dist/themes/data/catppuccin-frappe.js +177 -0
  139. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  140. package/dist/themes/data/catppuccin-latte.js +177 -0
  141. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  142. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  143. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  144. package/dist/themes/data/catppuccin-mocha.js +177 -0
  145. package/dist/themes/data/cyberpunk.d.ts +17 -0
  146. package/dist/themes/data/cyberpunk.js +193 -0
  147. package/dist/themes/data/default.d.ts +3 -0
  148. package/dist/themes/data/default.js +160 -0
  149. package/dist/themes/data/dracula.d.ts +3 -0
  150. package/dist/themes/data/dracula.js +177 -0
  151. package/dist/themes/data/flexoki.d.ts +3 -0
  152. package/dist/themes/data/flexoki.js +177 -0
  153. package/dist/themes/data/gruvbox.d.ts +3 -0
  154. package/dist/themes/data/gruvbox.js +177 -0
  155. package/dist/themes/data/index.d.ts +35 -0
  156. package/dist/themes/data/index.js +64 -0
  157. package/dist/themes/data/monokai.d.ts +3 -0
  158. package/dist/themes/data/monokai.js +177 -0
  159. package/dist/themes/data/nord.d.ts +3 -0
  160. package/dist/themes/data/nord.js +177 -0
  161. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  162. package/dist/themes/data/rose-pine-dawn.js +177 -0
  163. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  164. package/dist/themes/data/rose-pine-moon.js +177 -0
  165. package/dist/themes/data/rose-pine.d.ts +3 -0
  166. package/dist/themes/data/rose-pine.js +177 -0
  167. package/dist/themes/data/solarized-dark.d.ts +3 -0
  168. package/dist/themes/data/solarized-dark.js +177 -0
  169. package/dist/themes/data/solarized-light.d.ts +3 -0
  170. package/dist/themes/data/solarized-light.js +177 -0
  171. package/dist/themes/data/svg-export.d.ts +3 -0
  172. package/dist/themes/data/svg-export.js +177 -0
  173. package/dist/themes/data/textual-ansi.d.ts +3 -0
  174. package/dist/themes/data/textual-ansi.js +160 -0
  175. package/dist/themes/data/textual-dark.d.ts +3 -0
  176. package/dist/themes/data/textual-dark.js +160 -0
  177. package/dist/themes/data/textual-light.d.ts +3 -0
  178. package/dist/themes/data/textual-light.js +160 -0
  179. package/dist/themes/data/tokyo-night.d.ts +3 -0
  180. package/dist/themes/data/tokyo-night.js +177 -0
  181. package/dist/themes/data/types.d.ts +31 -0
  182. package/dist/themes/data/types.js +10 -0
  183. package/dist/themes/palette.d.ts +21 -0
  184. package/dist/themes/palette.js +28 -0
  185. package/dist/themes/ramp.d.ts +83 -0
  186. package/dist/themes/ramp.js +122 -0
  187. package/dist/themes/registry.d.ts +44 -0
  188. package/dist/themes/registry.js +94 -0
  189. package/dist/themes/terminalThemes.d.ts +49 -0
  190. package/dist/themes/terminalThemes.js +63 -0
  191. package/dist/themes/transpose.d.ts +87 -0
  192. package/dist/themes/transpose.js +138 -0
  193. package/dist/widgets/button.d.ts +49 -0
  194. package/dist/widgets/button.js +208 -0
  195. package/dist/widgets/checkbox.d.ts +41 -0
  196. package/dist/widgets/checkbox.js +157 -0
  197. package/dist/widgets/dropdown.d.ts +67 -0
  198. package/dist/widgets/dropdown.js +450 -0
  199. package/dist/widgets/event-router.d.ts +71 -0
  200. package/dist/widgets/event-router.js +629 -0
  201. package/dist/widgets/focus-manager.d.ts +45 -0
  202. package/dist/widgets/focus-manager.js +191 -0
  203. package/dist/widgets/hit.d.ts +64 -0
  204. package/dist/widgets/hit.js +113 -0
  205. package/dist/widgets/index.d.ts +20 -0
  206. package/dist/widgets/index.js +14 -0
  207. package/dist/widgets/slider.d.ts +57 -0
  208. package/dist/widgets/slider.js +270 -0
  209. package/dist/widgets/static-item.d.ts +34 -0
  210. package/dist/widgets/static-item.js +54 -0
  211. package/dist/widgets/text-input.d.ts +274 -0
  212. package/dist/widgets/text-input.js +1165 -0
  213. package/dist/widgets/toggle.d.ts +44 -0
  214. package/dist/widgets/toggle.js +180 -0
  215. package/dist/widgets/types.d.ts +87 -0
  216. package/dist/widgets/types.js +30 -0
  217. package/dist/widgets/widget-app.d.ts +63 -0
  218. package/dist/widgets/widget-app.js +199 -0
  219. package/dist/widgets/widget-base.d.ts +40 -0
  220. package/dist/widgets/widget-base.js +165 -0
  221. package/package.json +6 -2
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Segment — the atomic rendering unit. Every piece of styled terminal output
3
+ * is represented as a Segment: (text, style?, control?).
4
+ */
5
+ import { type CellCol } from "./cells.js";
6
+ import { Style } from "./style.js";
7
+ import { type Anchor } from "./anchor.js";
8
+ export declare enum ControlType {
9
+ BELL = "bell",
10
+ CARRIAGE_RETURN = "carriage_return",
11
+ HOME = "home",
12
+ CLEAR = "clear",
13
+ SHOW_CURSOR = "show_cursor",
14
+ HIDE_CURSOR = "hide_cursor",
15
+ ENABLE_ALT_SCREEN = "enable_alt_screen",
16
+ DISABLE_ALT_SCREEN = "disable_alt_screen",
17
+ CURSOR_UP = "cursor_up",
18
+ CURSOR_DOWN = "cursor_down",
19
+ CURSOR_FORWARD = "cursor_forward",
20
+ CURSOR_BACKWARD = "cursor_backward",
21
+ CURSOR_MOVE_TO_COLUMN = "cursor_move_to_column",
22
+ CURSOR_MOVE_TO = "cursor_move_to",
23
+ ERASE_IN_LINE = "erase_in_line",
24
+ SET_WINDOW_TITLE = "set_window_title"
25
+ }
26
+ export type ControlCode = [ControlType, ...unknown[]];
27
+ export declare class Segment {
28
+ readonly text: string;
29
+ readonly style: Style | undefined;
30
+ readonly control: ControlCode[] | undefined;
31
+ constructor(text: string, style?: Style, control?: ControlCode[]);
32
+ get cellLength(): number;
33
+ get hasText(): boolean;
34
+ get isControl(): boolean;
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.
42
+ */
43
+ splitCells(position: CellCol): [Segment, Segment];
44
+ private static _line;
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;
58
+ /**
59
+ * Yields segments with combined styles.
60
+ */
61
+ static applyStyle(segments: Iterable<Segment>, style?: Style, postStyle?: Style): Iterable<Segment>;
62
+ /**
63
+ * Filters segments by control status.
64
+ */
65
+ static filterControl(segments: Iterable<Segment>, isControl: boolean): Iterable<Segment>;
66
+ /**
67
+ * Splits segments at newlines. Yields arrays of segments per line.
68
+ */
69
+ static splitLines(segments: Iterable<Segment>): Segment[][];
70
+ /**
71
+ * Adjusts a line of segments to exactly `width` cells.
72
+ */
73
+ static adjustLineLength(line: Segment[], width: number, style?: Style, pad?: boolean): Segment[];
74
+ /**
75
+ * Crops a renderable's output so no line exceeds `width`, leaving short lines
76
+ * alone and the line structure exactly as it arrived.
77
+ *
78
+ * [LAW:single-enforcer] `adjustLineLength` is where a width is enforced, but
79
+ * it takes one line and a container holding arbitrary content has a stream of
80
+ * them. Without this step the container has to trust content to honour the
81
+ * offer, and content that ignores it overflows the region — which is how a
82
+ * `Layout` leaf and a `Tree` label came to emit forty cells into a one-cell
83
+ * offer while `Panel` and `Padding`, which split and adjust, did not.
84
+ *
85
+ * One segment in, one segment out, shortening text and nothing else — so the
86
+ * line structure that arrives is the line structure that leaves. Two earlier
87
+ * shapes of this both decided how many lines there were and both were wrong:
88
+ * built on `splitLines`, which cannot tell `"abc"` from `"abc\n"`, it invented
89
+ * a trailing newline and dropped the row of an empty `Tree` label entirely,
90
+ * running its guides into the next row; rebuilt to accumulate lines, it
91
+ * dropped the zero-width-but-present line `RichText` emits at an offer of 0,
92
+ * and a row split rendered nothing at all. A crop shortens; it does not count.
93
+ *
94
+ * A wide glyph the edge cuts through leaves a space in the cell it would have
95
+ * half-filled, so a cropped line still reaches the edge — the reference's
96
+ * `set_cell_size`, which `splitText` already is. An unbounded `width` crops
97
+ * nothing, and a segment the crop did not change leaves as itself.
98
+ */
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[][];
105
+ /**
106
+ * Returns total cell width of a line. Ignores control segments.
107
+ */
108
+ static getLineLength(line: Segment[]): number;
109
+ /**
110
+ * Returns [width, height] of a set of lines.
111
+ */
112
+ static getShape(lines: Segment[][]): [number, number];
113
+ /**
114
+ * Composes cells side by side into full lines. Each cell contributes its
115
+ * own lines cropped/padded to its declared width; a cell shorter than the
116
+ * tallest is filled with blank space so the grid stays aligned. Adjacent
117
+ * cells are separated by a gutter of spaces, and each merged row ends with a
118
+ * line break. This is the single side-by-side merge used by every horizontal
119
+ * layout (Columns grid rows, Layout row splits).
120
+ */
121
+ static mergeHorizontal(cells: readonly {
122
+ readonly lines: Segment[][];
123
+ readonly width: number;
124
+ }[], gutter?: number): Iterable<Segment>;
125
+ /**
126
+ * Merges contiguous segments with the same style.
127
+ */
128
+ static simplify(segments: Iterable<Segment>): Iterable<Segment>;
129
+ /**
130
+ * Yields segments with links removed.
131
+ */
132
+ static stripLinks(segments: Iterable<Segment>): Iterable<Segment>;
133
+ /**
134
+ * Yields segments with all styles removed.
135
+ */
136
+ static stripStyles(segments: Iterable<Segment>): Iterable<Segment>;
137
+ /**
138
+ * Yields segments with colors removed but attributes preserved.
139
+ */
140
+ static removeColor(segments: Iterable<Segment>): Iterable<Segment>;
141
+ /**
142
+ * Divides segments at cell positions. Yields arrays of segments for each section.
143
+ */
144
+ static divide(segments: Segment[], cuts: number[]): Segment[][];
145
+ /**
146
+ * Pads below to fill height.
147
+ */
148
+ static alignTop(lines: Segment[][], width: number, height: number, style: Style): Segment[][];
149
+ /**
150
+ * Pads above to fill height. Content at bottom.
151
+ */
152
+ static alignBottom(lines: Segment[][], width: number, height: number, style: Style): Segment[][];
153
+ /**
154
+ * Pads above and below. Content in middle.
155
+ */
156
+ static alignMiddle(lines: Segment[][], width: number, height: number, style: Style): Segment[][];
157
+ /**
158
+ * Forces lines to exactly width x height.
159
+ */
160
+ static setShape(lines: Segment[][], width: number, height: number, style?: Style): Segment[][];
161
+ }
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Segment — the atomic rendering unit. Every piece of styled terminal output
3
+ * is represented as a Segment: (text, style?, control?).
4
+ */
5
+ import { cellLen, splitText, asCellCol } from "./cells.js";
6
+ import { Style } from "./style.js";
7
+ import { shiftAnchor } from "./anchor.js";
8
+ // --- ControlType ---
9
+ export var ControlType;
10
+ (function (ControlType) {
11
+ ControlType["BELL"] = "bell";
12
+ ControlType["CARRIAGE_RETURN"] = "carriage_return";
13
+ ControlType["HOME"] = "home";
14
+ ControlType["CLEAR"] = "clear";
15
+ ControlType["SHOW_CURSOR"] = "show_cursor";
16
+ ControlType["HIDE_CURSOR"] = "hide_cursor";
17
+ ControlType["ENABLE_ALT_SCREEN"] = "enable_alt_screen";
18
+ ControlType["DISABLE_ALT_SCREEN"] = "disable_alt_screen";
19
+ ControlType["CURSOR_UP"] = "cursor_up";
20
+ ControlType["CURSOR_DOWN"] = "cursor_down";
21
+ ControlType["CURSOR_FORWARD"] = "cursor_forward";
22
+ ControlType["CURSOR_BACKWARD"] = "cursor_backward";
23
+ ControlType["CURSOR_MOVE_TO_COLUMN"] = "cursor_move_to_column";
24
+ ControlType["CURSOR_MOVE_TO"] = "cursor_move_to";
25
+ ControlType["ERASE_IN_LINE"] = "erase_in_line";
26
+ ControlType["SET_WINDOW_TITLE"] = "set_window_title";
27
+ })(ControlType || (ControlType = {}));
28
+ // --- Segment ---
29
+ export class Segment {
30
+ text;
31
+ style;
32
+ control;
33
+ constructor(text, style, control) {
34
+ this.text = text;
35
+ this.style = style;
36
+ this.control = control;
37
+ }
38
+ get cellLength() {
39
+ return this.control ? 0 : cellLen(this.text);
40
+ }
41
+ get hasText() {
42
+ return this.text.length > 0;
43
+ }
44
+ get isControl() {
45
+ return this.control !== undefined;
46
+ }
47
+ /**
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.
54
+ */
55
+ splitCells(position) {
56
+ const len = this.cellLength;
57
+ if (position >= len)
58
+ return [this, new Segment("")];
59
+ if (position <= 0)
60
+ return [new Segment(""), this];
61
+ const [leftText, rightText] = splitText(this.text, position);
62
+ return [
63
+ new Segment(leftText, this.style),
64
+ new Segment(rightText, this.style?.shiftedBy(len - cellLen(rightText))),
65
+ ];
66
+ }
67
+ // --- Static factories ---
68
+ static _line;
69
+ static line() {
70
+ return (Segment._line ??= new Segment("\n"));
71
+ }
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
+ }
111
+ /**
112
+ * Yields segments with combined styles.
113
+ */
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
+ }
121
+ for (const segment of segments) {
122
+ if (segment.isControl) {
123
+ yield segment;
124
+ continue;
125
+ }
126
+ let s = segment.style;
127
+ if (style)
128
+ s = style.add(s);
129
+ if (postStyle)
130
+ s = s ? s.add(postStyle) : postStyle;
131
+ yield new Segment(segment.text, s, segment.control);
132
+ }
133
+ }
134
+ /**
135
+ * Filters segments by control status.
136
+ */
137
+ static *filterControl(segments, isControl) {
138
+ for (const segment of segments) {
139
+ if (segment.isControl === isControl)
140
+ yield segment;
141
+ }
142
+ }
143
+ /**
144
+ * Splits segments at newlines. Yields arrays of segments per line.
145
+ */
146
+ static splitLines(segments) {
147
+ const lines = [];
148
+ let currentLine = [];
149
+ for (const segment of segments) {
150
+ if (segment.isControl) {
151
+ currentLine.push(segment);
152
+ continue;
153
+ }
154
+ const text = segment.text;
155
+ if (!text.includes("\n")) {
156
+ currentLine.push(segment);
157
+ continue;
158
+ }
159
+ const parts = text.split("\n");
160
+ for (let i = 0; i < parts.length; i++) {
161
+ if (i > 0) {
162
+ lines.push(currentLine);
163
+ currentLine = [];
164
+ }
165
+ const part = parts[i];
166
+ if (part.length > 0) {
167
+ currentLine.push(new Segment(part, segment.style));
168
+ }
169
+ }
170
+ }
171
+ if (currentLine.length > 0) {
172
+ lines.push(currentLine);
173
+ }
174
+ return lines;
175
+ }
176
+ /**
177
+ * Adjusts a line of segments to exactly `width` cells.
178
+ */
179
+ static adjustLineLength(line, width, style, pad = true) {
180
+ const currentWidth = Segment.getLineLength(line);
181
+ if (currentWidth === width)
182
+ return line;
183
+ if (currentWidth < width) {
184
+ if (!pad)
185
+ return line;
186
+ return [...line, new Segment(" ".repeat(width - currentWidth), style)];
187
+ }
188
+ // Crop to width
189
+ const result = [];
190
+ let remaining = width;
191
+ for (const segment of line) {
192
+ if (segment.isControl) {
193
+ result.push(segment);
194
+ continue;
195
+ }
196
+ const segWidth = segment.cellLength;
197
+ if (segWidth <= remaining) {
198
+ result.push(segment);
199
+ remaining -= segWidth;
200
+ }
201
+ else {
202
+ const [left] = segment.splitCells(asCellCol(remaining));
203
+ result.push(left);
204
+ remaining = 0;
205
+ break;
206
+ }
207
+ }
208
+ return result;
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.
226
+ /**
227
+ * Crops a renderable's output so no line exceeds `width`, leaving short lines
228
+ * alone and the line structure exactly as it arrived.
229
+ *
230
+ * [LAW:single-enforcer] `adjustLineLength` is where a width is enforced, but
231
+ * it takes one line and a container holding arbitrary content has a stream of
232
+ * them. Without this step the container has to trust content to honour the
233
+ * offer, and content that ignores it overflows the region — which is how a
234
+ * `Layout` leaf and a `Tree` label came to emit forty cells into a one-cell
235
+ * offer while `Panel` and `Padding`, which split and adjust, did not.
236
+ *
237
+ * One segment in, one segment out, shortening text and nothing else — so the
238
+ * line structure that arrives is the line structure that leaves. Two earlier
239
+ * shapes of this both decided how many lines there were and both were wrong:
240
+ * built on `splitLines`, which cannot tell `"abc"` from `"abc\n"`, it invented
241
+ * a trailing newline and dropped the row of an empty `Tree` label entirely,
242
+ * running its guides into the next row; rebuilt to accumulate lines, it
243
+ * dropped the zero-width-but-present line `RichText` emits at an offer of 0,
244
+ * and a row split rendered nothing at all. A crop shortens; it does not count.
245
+ *
246
+ * A wide glyph the edge cuts through leaves a space in the cell it would have
247
+ * half-filled, so a cropped line still reaches the edge — the reference's
248
+ * `set_cell_size`, which `splitText` already is. An unbounded `width` crops
249
+ * nothing, and a segment the crop did not change leaves as itself.
250
+ */
251
+ static *cropLines(segments, width) {
252
+ const cap = Math.max(0, width);
253
+ let used = 0;
254
+ for (const segment of segments) {
255
+ if (segment.isControl) {
256
+ yield segment;
257
+ continue;
258
+ }
259
+ const parts = segment.text.split("\n");
260
+ for (let i = 0; i < parts.length; i++) {
261
+ if (i > 0)
262
+ used = 0;
263
+ const [piece] = splitText(parts[i], asCellCol(cap - used));
264
+ used += cellLen(piece);
265
+ parts[i] = piece;
266
+ }
267
+ const cropped = parts.join("\n");
268
+ yield cropped === segment.text ? segment : new Segment(cropped, segment.style);
269
+ }
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
+ }
278
+ /**
279
+ * Returns total cell width of a line. Ignores control segments.
280
+ */
281
+ static getLineLength(line) {
282
+ let width = 0;
283
+ for (const segment of line) {
284
+ if (!segment.isControl) {
285
+ width += segment.cellLength;
286
+ }
287
+ }
288
+ return width;
289
+ }
290
+ /**
291
+ * Returns [width, height] of a set of lines.
292
+ */
293
+ static getShape(lines) {
294
+ if (lines.length === 0)
295
+ return [0, 0];
296
+ let maxWidth = 0;
297
+ for (const line of lines) {
298
+ const w = Segment.getLineLength(line);
299
+ if (w > maxWidth)
300
+ maxWidth = w;
301
+ }
302
+ return [maxWidth, lines.length];
303
+ }
304
+ /**
305
+ * Composes cells side by side into full lines. Each cell contributes its
306
+ * own lines cropped/padded to its declared width; a cell shorter than the
307
+ * tallest is filled with blank space so the grid stays aligned. Adjacent
308
+ * cells are separated by a gutter of spaces, and each merged row ends with a
309
+ * line break. This is the single side-by-side merge used by every horizontal
310
+ * layout (Columns grid rows, Layout row splits).
311
+ */
312
+ static *mergeHorizontal(cells, gutter = 0) {
313
+ let height = 0;
314
+ for (const cell of cells) {
315
+ if (cell.lines.length > height)
316
+ height = cell.lines.length;
317
+ }
318
+ const gap = gutter > 0 ? [new Segment(" ".repeat(gutter))] : [];
319
+ for (let row = 0; row < height; row++) {
320
+ for (let col = 0; col < cells.length; col++) {
321
+ if (col > 0)
322
+ yield* gap;
323
+ const cell = cells[col];
324
+ yield* Segment.adjustLineLength(cell.lines[row] ?? [], cell.width);
325
+ }
326
+ yield Segment.line();
327
+ }
328
+ }
329
+ /**
330
+ * Merges contiguous segments with the same style.
331
+ */
332
+ static *simplify(segments) {
333
+ let pending;
334
+ for (const segment of segments) {
335
+ if (!pending) {
336
+ pending = segment;
337
+ continue;
338
+ }
339
+ if (stylesEqual(pending.style, segment.style) && !segment.isControl && !pending.isControl) {
340
+ pending = new Segment(pending.text + segment.text, pending.style);
341
+ }
342
+ else {
343
+ yield pending;
344
+ pending = segment;
345
+ }
346
+ }
347
+ if (pending)
348
+ yield pending;
349
+ }
350
+ /**
351
+ * Yields segments with links removed.
352
+ */
353
+ static *stripLinks(segments) {
354
+ for (const segment of segments) {
355
+ if (segment.style?.link) {
356
+ yield new Segment(segment.text, segment.style.clearMetaAndLinks(), segment.control);
357
+ }
358
+ else {
359
+ yield segment;
360
+ }
361
+ }
362
+ }
363
+ /**
364
+ * Yields segments with all styles removed.
365
+ */
366
+ static *stripStyles(segments) {
367
+ for (const segment of segments) {
368
+ yield new Segment(segment.text, undefined, segment.control);
369
+ }
370
+ }
371
+ /**
372
+ * Yields segments with colors removed but attributes preserved.
373
+ */
374
+ static *removeColor(segments) {
375
+ for (const segment of segments) {
376
+ if (segment.style) {
377
+ yield new Segment(segment.text, segment.style.withoutColor, segment.control);
378
+ }
379
+ else {
380
+ yield segment;
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Divides segments at cell positions. Yields arrays of segments for each section.
386
+ */
387
+ static divide(segments, cuts) {
388
+ if (cuts.length === 0)
389
+ return [segments];
390
+ const result = [];
391
+ let segmentIndex = 0;
392
+ let cellOffset = 0;
393
+ let currentSegment = segments[segmentIndex];
394
+ for (const cut of cuts) {
395
+ const section = [];
396
+ while (currentSegment && cellOffset + currentSegment.cellLength <= cut) {
397
+ if (currentSegment.isControl) {
398
+ section.push(currentSegment);
399
+ }
400
+ else {
401
+ section.push(currentSegment);
402
+ cellOffset += currentSegment.cellLength;
403
+ }
404
+ segmentIndex++;
405
+ currentSegment = segments[segmentIndex];
406
+ }
407
+ if (currentSegment && !currentSegment.isControl && cellOffset < cut) {
408
+ const splitAt = asCellCol(cut - cellOffset);
409
+ const [left, right] = currentSegment.splitCells(splitAt);
410
+ if (left.hasText)
411
+ section.push(left);
412
+ cellOffset += left.cellLength;
413
+ currentSegment = right;
414
+ }
415
+ result.push(section);
416
+ }
417
+ // Remaining segments
418
+ const tail = [];
419
+ if (currentSegment?.hasText)
420
+ tail.push(currentSegment);
421
+ segmentIndex++;
422
+ while (segmentIndex < segments.length) {
423
+ tail.push(segments[segmentIndex]);
424
+ segmentIndex++;
425
+ }
426
+ if (tail.length > 0)
427
+ result.push(tail);
428
+ return result;
429
+ }
430
+ // --- Layout helpers ---
431
+ /**
432
+ * Pads below to fill height.
433
+ */
434
+ static alignTop(lines, width, height, style) {
435
+ const result = lines.map((line) => Segment.adjustLineLength(line, width, style));
436
+ const blankLine = [new Segment(" ".repeat(width), style)];
437
+ while (result.length < height) {
438
+ result.push([...blankLine]);
439
+ }
440
+ return result.slice(0, height);
441
+ }
442
+ /**
443
+ * Pads above to fill height. Content at bottom.
444
+ */
445
+ static alignBottom(lines, width, height, style) {
446
+ const adjusted = lines.map((line) => Segment.adjustLineLength(line, width, style));
447
+ const blankLine = [new Segment(" ".repeat(width), style)];
448
+ const padCount = Math.max(0, height - adjusted.length);
449
+ const result = [];
450
+ for (let i = 0; i < padCount; i++) {
451
+ result.push([...blankLine]);
452
+ }
453
+ result.push(...adjusted);
454
+ return result.slice(0, height);
455
+ }
456
+ /**
457
+ * Pads above and below. Content in middle.
458
+ */
459
+ static alignMiddle(lines, width, height, style) {
460
+ const adjusted = lines.map((line) => Segment.adjustLineLength(line, width, style));
461
+ const blankLine = [new Segment(" ".repeat(width), style)];
462
+ const padAbove = Math.max(0, Math.floor((height - adjusted.length) / 2));
463
+ const result = [];
464
+ for (let i = 0; i < padAbove; i++) {
465
+ result.push([...blankLine]);
466
+ }
467
+ result.push(...adjusted);
468
+ while (result.length < height) {
469
+ result.push([...blankLine]);
470
+ }
471
+ return result.slice(0, height);
472
+ }
473
+ /**
474
+ * Forces lines to exactly width x height.
475
+ */
476
+ static setShape(lines, width, height, style) {
477
+ const result = lines.map((line) => Segment.adjustLineLength(line, width, style));
478
+ const blankLine = [new Segment(" ".repeat(width), style)];
479
+ while (result.length < height) {
480
+ result.push([...blankLine]);
481
+ }
482
+ return result.slice(0, height);
483
+ }
484
+ }
485
+ // --- Internal helpers ---
486
+ function stylesEqual(a, b) {
487
+ if (a === b)
488
+ return true;
489
+ if (!a || !b)
490
+ return false;
491
+ return a.equals(b);
492
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Built-in spinner frame data.
3
+ * [LAW:one-type-per-behavior] All spinners are instances of SpinnerData — differ only by frame data.
4
+ */
5
+ export interface SpinnerData {
6
+ readonly frames: readonly string[];
7
+ readonly interval: number;
8
+ }
9
+ /** The spinner an ASCII-only output draws in place of any other. */
10
+ export declare const LINE_SPINNER: SpinnerData;
11
+ export declare const SPINNERS: Record<string, SpinnerData>;
12
+ export declare const DEFAULT_SPINNER = "dots";