@promptctl/rich-js 0.18.0 → 0.19.0

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 (244) hide show
  1. package/README.md +9 -7
  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 +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Viewport — a fixed number of rows onto a taller renderable, scrolled by an
3
+ * offset that cannot leave the content.
4
+ *
5
+ * Its rows, under the `Height` contract in `../core/protocol.ts`: the region's
6
+ * when it is given one; otherwise its own configured rows, or the content's
7
+ * full length when it has none, capped by any ceiling. It renders its content
8
+ * with no height, because the content's natural length is the scroll extent,
9
+ * and pads or crops what comes back to its rows — its rows are its own height,
10
+ * not a region someone else will shape.
11
+ *
12
+ * Its width is the width it is given, in both directions: each row is cropped
13
+ * or padded to it, so content that ignores its width cannot spill past the
14
+ * viewport's edge and the scrollbar, drawn in a gutter at that edge, lines up
15
+ * down every row. [LAW:dataflow-not-control-flow] A viewport with no scrollbar
16
+ * has a gutter zero cells wide, and every row takes the same path either way.
17
+ *
18
+ * [LAW:no-ambient-temporal-coupling] Neither half of what an offset is clamped
19
+ * against exists before a render: the rows come from the budget the render is
20
+ * handed, and the content's length from rendering it at the width it is handed.
21
+ * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
+ * are called. Each queues a move, and the next render resolves the queue in
23
+ * call order, clamping after every move, against the rows and length that
24
+ * render found. Calling an operation before the first render, or three of them
25
+ * between two renders, is the same code path as calling one.
26
+ *
27
+ * Every cell of its rows, gutter included, is stamped as its own
28
+ * (`Segment.anchorLines`), around whatever anchors its content drew. So the
29
+ * composed frame names the viewport under any of its cells however deep it is
30
+ * nested, and a cell of a widget inside it names both: that is how the wheel
31
+ * finds the viewport to scroll. `canScrollBy` is how it passes over one that
32
+ * cannot move that way, to the viewport around it.
33
+ */
34
+ import { Segment } from "../core/segment.js";
35
+ import { Measurement } from "../core/measure.js";
36
+ import { cellCount, cellLen } from "../core/cells.js";
37
+ import { NULL_STYLE } from "../core/style.js";
38
+ import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
39
+ /** A heavy line for the thumb on a light line for the track. */
40
+ export const SCROLLBAR = {
41
+ thumb: { glyph: "┃", style: "scrollbar.thumb" },
42
+ track: { glyph: "│", style: "scrollbar.track" },
43
+ };
44
+ /** The absence of a scrollbar, as a gutter zero cells wide. */
45
+ const NO_SCROLLBAR = {
46
+ thumb: { glyph: "", style: NULL_STYLE },
47
+ track: { glyph: "", style: NULL_STYLE },
48
+ };
49
+ export class Viewport {
50
+ /**
51
+ * What the viewport shows. Replace it to show new content from the same
52
+ * scroll position — a view rebuilt every frame keeps one `Viewport`.
53
+ */
54
+ content;
55
+ rows;
56
+ scrollbar;
57
+ _offset = 0;
58
+ // Nothing rendered, nothing shown: no move can take until the first render.
59
+ _extent = { rows: 0, lines: 0 };
60
+ _moves = [];
61
+ constructor(content, options = {}) {
62
+ this.content = content;
63
+ this.rows = options.rows;
64
+ this.scrollbar = options.scrollbar ?? NO_SCROLLBAR;
65
+ }
66
+ /**
67
+ * The first content line the last render showed. A move requested since is
68
+ * not reflected until the next render resolves it.
69
+ */
70
+ get offset() {
71
+ return this._offset;
72
+ }
73
+ /** Scroll so `line` is the first line shown. */
74
+ scrollTo(line) {
75
+ this._moves.push(() => line);
76
+ }
77
+ /** Scroll by `lines`: down when positive, up when negative. */
78
+ scrollBy(lines) {
79
+ this._moves.push((offset) => offset + lines);
80
+ }
81
+ /**
82
+ * Whether `scrollBy(lines)` would move the viewport from where the moves
83
+ * queued so far leave it, against the rows and length the last render
84
+ * found.
85
+ */
86
+ canScrollBy(lines) {
87
+ const offset = this.resolve(this._extent);
88
+ return clampOffset(offset + lines, this._extent) !== offset;
89
+ }
90
+ /**
91
+ * Scroll the least distance that shows lines `start` up to but not
92
+ * including `end` — lines of the content as it renders at the viewport's
93
+ * width, so an item that wraps spans more than one. A range already in view does not move; one taller than
94
+ * the viewport shows its first line at the top.
95
+ */
96
+ ensureVisible(start, end) {
97
+ this._moves.push((offset, { rows }) => Math.min(start, Math.max(offset, end - rows)));
98
+ }
99
+ /**
100
+ * The width its content renders at, out of `width`: what the scrollbar's
101
+ * gutter leaves. Lines handed to `ensureVisible` are lines at this width.
102
+ */
103
+ contentWidth(width) {
104
+ return cellCount(width - gutterWidth(this.scrollbar));
105
+ }
106
+ *render(rawOptions) {
107
+ const { height, ...options } = withBoundedWidth(rawOptions, this);
108
+ const gutter = gutterWidth(this.scrollbar);
109
+ const contentWidth = this.contentWidth(options.maxWidth);
110
+ const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
111
+ const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
112
+ this._extent = extent;
113
+ this._offset = this.resolve(extent);
114
+ this._moves = [];
115
+ const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
116
+ // [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
117
+ // this render just resolved, not from `offset` as it stood before it.
118
+ const thumb = thumbRows(extent, this._offset);
119
+ // The content's cells and the gutter's drawn cells sum to the offer: a
120
+ // gutter wider than the whole offer is drawn cropped to it.
121
+ const drawn = Math.min(gutter, options.maxWidth);
122
+ // The style reaches the padding too, so a glyph narrower than the gutter
123
+ // still fills its cell with its part's background.
124
+ const cell = ({ glyph, style }) => {
125
+ const resolved = getStyle(options, style);
126
+ return Segment.adjustLineLength([new Segment(glyph, resolved)], drawn, resolved);
127
+ };
128
+ // The gutter keeps the chosen scrollbar's width, so an ASCII stand-in
129
+ // padded into it leaves the content where `contentWidth` said it would be.
130
+ const bar = drawable(options, this.scrollbar, asciiScrollbar(this.scrollbar), scrollbarGlyphs);
131
+ const thumbCell = cell(bar.thumb);
132
+ const trackCell = cell(bar.track);
133
+ const rows = shown.map((line, row) => [
134
+ ...Segment.adjustLineLength(line, contentWidth),
135
+ ...(row >= thumb.start && row < thumb.end ? thumbCell : trackCell),
136
+ ]);
137
+ for (const line of Segment.anchorLines(rows, this)) {
138
+ yield* line;
139
+ yield Segment.line();
140
+ }
141
+ }
142
+ /**
143
+ * [LAW:one-source-of-truth] Where the queued moves leave the offset against
144
+ * `extent`: what a render commits, and what `canScrollBy` asks from.
145
+ * [LAW:dataflow-not-control-flow] The offset is re-clamped with or without
146
+ * queued moves: content that shrank since the last render pulls it back.
147
+ */
148
+ resolve(extent) {
149
+ return this._moves.reduce((offset, move) => clampOffset(move(offset, extent), extent), clampOffset(this._offset, extent));
150
+ }
151
+ measure(rawOptions) {
152
+ // Its width is its content's and its gutter's. `withCellWidth` and not
153
+ // `withBoundedWidth`: the bounded parse asks this very method for the
154
+ // natural width.
155
+ const options = withCellWidth(rawOptions);
156
+ const gutter = gutterWidth(this.scrollbar);
157
+ const inner = { ...options, maxWidth: this.contentWidth(options.maxWidth) };
158
+ const content = Measurement.get(inner, this.content);
159
+ return new Measurement(content.minimum + gutter, content.maximum + gutter).withMaximum(options.maxWidth);
160
+ }
161
+ }
162
+ const scrollbarGlyphs = ({ thumb, track }) => thumb.glyph + track.glyph;
163
+ /** `scrollbar` in its own styles, drawn in ASCII. */
164
+ function asciiScrollbar({ thumb, track }) {
165
+ return { thumb: { ...thumb, glyph: "#" }, track: { ...track, glyph: "|" } };
166
+ }
167
+ /** The cells a scrollbar's gutter takes from the content: its wider glyph. */
168
+ function gutterWidth({ thumb, track }) {
169
+ return Math.max(cellLen(thumb.glyph), cellLen(track.glyph));
170
+ }
171
+ /**
172
+ * The rows of the track the thumb covers, from `start` up to but not including
173
+ * `end`: its length is the share of the content in view, and its position the
174
+ * share of the scroll travelled. Content that fits fills the track.
175
+ *
176
+ * An end of the track means an end of the content. Rounded alone, the position
177
+ * reached the bottom while lines were still hidden below — at 10 rows of 20
178
+ * lines, offset 9 rounds onto the last position. So an offset short of an end
179
+ * is held one row off it, wherever the track has a row between its ends.
180
+ */
181
+ function thumbRows({ rows, lines }, offset) {
182
+ const size = Math.min(rows, Math.max(1, Math.round((rows * rows) / Math.max(lines, rows, 1))));
183
+ const travel = rows - size;
184
+ const last = Math.max(1, lines - rows);
185
+ const proportional = Math.round((offset * travel) / last);
186
+ const start = Math.max(Math.min(proportional, travel - Math.min(last - offset, 1)), Math.min(offset, 1, travel));
187
+ return { start, end: start + size };
188
+ }
189
+ /** The rows a viewport shows, from its budget, its own configured rows, and its content's length. */
190
+ function viewRows(height, rows, lines) {
191
+ return regionRows(height) ?? cellCount(Math.min(rows ?? lines, height?.rows ?? Infinity));
192
+ }
193
+ /** An offset held to the lines that exist: never above the first, never past the last full view. */
194
+ function clampOffset(offset, { rows, lines }) {
195
+ return cellCount(Math.min(offset, lines - rows));
196
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Color *values* for the rich-js template binding.
3
+ *
4
+ * ### Why colors are values
5
+ *
6
+ * Every other function in this binding layer takes a child fragment and
7
+ * returns a styled fragment. These do not: they take colors and return a
8
+ * color. That is the whole point. Before this module existed, a color could
9
+ * not survive between two operations — every color function resolved and
10
+ * applied in one breath — so composing two color operations had to be spelled
11
+ * as a *string grammar* (`"primary-darken-3 50%"`), a second language
12
+ * reimplementing function application inside a string literal. Every new color
13
+ * operation meant a new grammar production. [LAW:no-mode-explosion]
14
+ *
15
+ * With colors as values the surface is one shape that grows by nesting:
16
+ *
17
+ * ```
18
+ * {{ fg (color "primary") .branch }}
19
+ * {{ fg (darken (color "primary") 2) .branch }}
20
+ * {{ fg (mix (color "foreground") (color "surface") 65) .label }}
21
+ * ```
22
+ *
23
+ * The simple case is a literal prefix of the composed case — an author adding
24
+ * a `darken` to working code wraps it, rather than rewriting it into an
25
+ * unfamiliar form. [LAW:composability]
26
+ *
27
+ * ### The carrier is a hex string, deliberately
28
+ *
29
+ * A color crosses the template seam as `#RRGGBB` (or `#RRGGBBAA`), not as an opaque object,
30
+ * for three reasons:
31
+ *
32
+ * 1. **The type separates.** `"string"` is the strictest arg slot the template
33
+ * engine has — it *refuses* typed `T`. So a color slot cannot silently
34
+ * accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
35
+ * both of which admit any non-primitive, and `{{ bold (color "primary") }}`
36
+ * would type-check its way into nonsense. [LAW:types-are-the-program]
37
+ * 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
38
+ * `printf` prints it, `eq` compares it — no engine changes required. Naming
39
+ * a color once and using it ten times is the ergonomic payoff.
40
+ * 3. **Misuse is visible.** A color that lands in text position renders as
41
+ * `#7aa2f7` — wrong in a way an author immediately sees, never a silently
42
+ * dropped style. [LAW:no-silent-failure]
43
+ *
44
+ * No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
45
+ * take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
46
+ * axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
47
+ * of them quantizes visibly. Compose two, or reach for `transposePalette` when
48
+ * a whole palette needs adapting — the same caveat that already applies to
49
+ * chained transpositions.
50
+ *
51
+ * ### Palette-free on purpose
52
+ *
53
+ * Nothing here knows what a palette is. `color` and `ramp` (the two
54
+ * palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
55
+ * from that point on the math is pure. A consumer with no theme system at all still gets
56
+ * the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
57
+ */
58
+ import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
59
+ import { ColorDepth, type ColorRgba, type TerminalTheme } from "../core/color.js";
60
+ /**
61
+ * A colour the colour functions can compute with. A fully transparent one — a
62
+ * theme's "no background", `#00000000` — has no colour of its own: what shows
63
+ * through it is whatever surface it lands on, which a template cannot know.
64
+ * Darkening, mixing or contrasting it would answer for a surface nobody named,
65
+ * so it is refused here, for every colour function and every `ramp` stop.
66
+ * [LAW:single-enforcer]
67
+ */
68
+ export declare function computable(colour: ColorRgba, func: string, written: string): ColorRgba;
69
+ /**
70
+ * Where `readableOn`'s text will be drawn: the depth the output is encoded at,
71
+ * and the terminal that draws it when that is known. At sixteen colours a
72
+ * terminal paints each slot in its own theme's colour, so the floor is measured
73
+ * in `terminal`'s sixteen; without one, in the VGA defaults (the Windows
74
+ * console's at `WINDOWS`). Both are facts about the one place the text lands,
75
+ * so they arrive together.
76
+ */
77
+ export interface DrawnAt {
78
+ readonly depth: ColorDepth;
79
+ readonly terminal?: TerminalTheme;
80
+ }
81
+ /**
82
+ * The `readableOn` binding, measuring where `drawnAt` says the text lands when
83
+ * it is evaluated — a consumer that renders to a per-call destination
84
+ * registers its own.
85
+ */
86
+ export declare const readableOnFunc: (drawnAt: () => DrawnAt) => TemplateFunc;
87
+ /**
88
+ * The palette-free color vocabulary: every function takes colors and returns a
89
+ * color, so they compose by nesting.
90
+ *
91
+ * | function | meaning |
92
+ * |---|---|
93
+ * | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
94
+ * | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
95
+ * | `contrastOn bg` | black or white, whichever is readable on `bg` |
96
+ * | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
97
+ * | `shiftHue c deg` | rotate hue in OKLCH |
98
+ * | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
99
+ * | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
100
+ * | `shiftLightness c d` | add to lightness, after any scale |
101
+ *
102
+ * Pair with `paletteFuncs()` to name colors from a theme, and with
103
+ * `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
104
+ *
105
+ * @param drawnAt where the text will be drawn — the depth, and the terminal
106
+ * when known — read on every `readableOn` evaluation so a host that learns
107
+ * its destination per render passes one getter; defaults to truecolor.
108
+ */
109
+ export declare function colorFuncs(drawnAt?: () => DrawnAt): FuncMap;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Color *values* for the rich-js template binding.
3
+ *
4
+ * ### Why colors are values
5
+ *
6
+ * Every other function in this binding layer takes a child fragment and
7
+ * returns a styled fragment. These do not: they take colors and return a
8
+ * color. That is the whole point. Before this module existed, a color could
9
+ * not survive between two operations — every color function resolved and
10
+ * applied in one breath — so composing two color operations had to be spelled
11
+ * as a *string grammar* (`"primary-darken-3 50%"`), a second language
12
+ * reimplementing function application inside a string literal. Every new color
13
+ * operation meant a new grammar production. [LAW:no-mode-explosion]
14
+ *
15
+ * With colors as values the surface is one shape that grows by nesting:
16
+ *
17
+ * ```
18
+ * {{ fg (color "primary") .branch }}
19
+ * {{ fg (darken (color "primary") 2) .branch }}
20
+ * {{ fg (mix (color "foreground") (color "surface") 65) .label }}
21
+ * ```
22
+ *
23
+ * The simple case is a literal prefix of the composed case — an author adding
24
+ * a `darken` to working code wraps it, rather than rewriting it into an
25
+ * unfamiliar form. [LAW:composability]
26
+ *
27
+ * ### The carrier is a hex string, deliberately
28
+ *
29
+ * A color crosses the template seam as `#RRGGBB` (or `#RRGGBBAA`), not as an opaque object,
30
+ * for three reasons:
31
+ *
32
+ * 1. **The type separates.** `"string"` is the strictest arg slot the template
33
+ * engine has — it *refuses* typed `T`. So a color slot cannot silently
34
+ * accept a text fragment. An opaque object would land in `"T"`/`"liftable"`,
35
+ * both of which admit any non-primitive, and `{{ bold (color "primary") }}`
36
+ * would type-check its way into nonsense. [LAW:types-are-the-program]
37
+ * 2. **It flows through the language.** `{{ $muted := mix … }}` holds it,
38
+ * `printf` prints it, `eq` compares it — no engine changes required. Naming
39
+ * a color once and using it ten times is the ergonomic payoff.
40
+ * 3. **Misuse is visible.** A color that lands in text position renders as
41
+ * `#7aa2f7` — wrong in a way an author immediately sees, never a silently
42
+ * dropped style. [LAW:no-silent-failure]
43
+ *
44
+ * No precision is given up: `darken`, `blendRgb`, and `contrastFor` already
45
+ * take and return 8-bit `ColorRgba`. The one caveat is OKLCH: each of the
46
+ * axis functions below round-trips sRGB→OKLCH→sRGB, so chaining three or more
47
+ * of them quantizes visibly. Compose two, or reach for `transposePalette` when
48
+ * a whole palette needs adapting — the same caveat that already applies to
49
+ * chained transpositions.
50
+ *
51
+ * ### Palette-free on purpose
52
+ *
53
+ * Nothing here knows what a palette is. `color` and `ramp` (the two
54
+ * palette-dependent functions, in `palette-funcs.ts`) turn names into hex;
55
+ * from that point on the math is pure. A consumer with no theme system at all still gets
56
+ * the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
57
+ */
58
+ import { blendRgb, ColorDepth, ColorParseError, parseHexColor } from "../core/color.js";
59
+ import { Oklch, IDENTITY } from "../core/oklch.js";
60
+ import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
61
+ /**
62
+ * The gate every color argument crosses. [LAW:parse-dont-validate] — the
63
+ * function bodies below receive `ColorRgba` and never re-check.
64
+ *
65
+ * Parsing itself is `parseHexColor`'s job; this wrapper exists only to replace
66
+ * its error *wording*, because the mistake it catches is almost always the
67
+ * same one — a palette name passed where a color belongs (`darken "primary" 2`)
68
+ * — and an author who reads "wrap it: darken (color "primary")" is corrected in
69
+ * one step, where "expected #RRGGBB" leaves them guessing. What it parses
70
+ * must then be a colour these functions can compute with (`computable`).
71
+ * [LAW:no-silent-failure]
72
+ */
73
+ function asColor(value, func) {
74
+ let colour;
75
+ try {
76
+ colour = parseHexColor(value.trim());
77
+ }
78
+ catch (cause) {
79
+ if (!(cause instanceof ColorParseError))
80
+ throw cause;
81
+ throw new TypeError(`${func} expected a color (#RRGGBB or #RRGGBBAA), got ${JSON.stringify(value)}` +
82
+ ` — to use a palette name here, wrap it: ${func} (color ${JSON.stringify(value)}) …`, { cause });
83
+ }
84
+ return computable(colour, func, JSON.stringify(value));
85
+ }
86
+ /**
87
+ * A colour the colour functions can compute with. A fully transparent one — a
88
+ * theme's "no background", `#00000000` — has no colour of its own: what shows
89
+ * through it is whatever surface it lands on, which a template cannot know.
90
+ * Darkening, mixing or contrasting it would answer for a surface nobody named,
91
+ * so it is refused here, for every colour function and every `ramp` stop.
92
+ * [LAW:single-enforcer]
93
+ */
94
+ export function computable(colour, func, written) {
95
+ if (colour.alpha === 0) {
96
+ throw new RangeError(`${func} got ${written}, which is fully transparent: it draws no colour of its own, ` +
97
+ `so there is nothing to compute with — use the colour of the surface it sits on, e.g. (color "background")`);
98
+ }
99
+ return colour;
100
+ }
101
+ // [LAW:single-enforcer] Every colour function is fixed-arity, so the engine's
102
+ // arity gate refuses a missing or surplus argument before the body runs.
103
+ function colorFunc(argTypes, fn) {
104
+ return { fn, argTypes, arity: { kind: "exact" }, returnType: "string" };
105
+ }
106
+ /**
107
+ * The gate every *numeric* argument crosses, with an optional inclusive range.
108
+ *
109
+ * [LAW:single-enforcer] One check for every `"float"` slot. An `"int"` slot is
110
+ * a safe integer by the engine's gate and needs none, but a `"float"` slot admits
111
+ * NaN and Infinity (legitimate IEEE-754 values), which would otherwise surface
112
+ * as `ColorRgba.red must be an integer in [0, 255]; got NaN` — loud, but
113
+ * pointing at a channel the author never wrote. [LAW:no-silent-failure]
114
+ */
115
+ function asAmount(value, func, param, range) {
116
+ const inRange = range === undefined || (value >= range.min && value <= range.max);
117
+ if (!Number.isFinite(value) || !inRange) {
118
+ const bound = range === undefined
119
+ ? "a finite number"
120
+ : `within ${range.min}..${range.max} (${range.note})`;
121
+ throw new RangeError(`${func}'s ${param} must be ${bound}, got ${value}`);
122
+ }
123
+ return value;
124
+ }
125
+ // --- HSL lightness ---
126
+ //
127
+ // `lighten` is `darken` with the sign flipped — the same operation, not a
128
+ // second one (colorMath's `lighten` is literally `darken(c, -levels)`). Both
129
+ // names ship because "lighten by 2" and "darken by -2" are not equally
130
+ // readable at a call site, and the operation is genuinely bidirectional.
131
+ const darkenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "darken"), levels).hex));
132
+ const lightenFunc = colorFunc(["string", "int"], ((hex, levels) => darken(asColor(hex, "lighten"), -levels).hex));
133
+ // --- Blending ---
134
+ //
135
+ // [LAW:one-type-per-behavior] There is no separate `alphaBlend` binding.
136
+ // `alphaBlend(fg, bg, a)` in colorMath is *defined* as `blendRgb(bg, fg, a)` —
137
+ // one operation wearing two names. Compositing a translucent color over a
138
+ // background and mixing two colors by a percentage are the same arithmetic,
139
+ // so they get one function. Terminals have no alpha channel, which is also why
140
+ // no function here *produces* a translucent color: it could not be rendered,
141
+ // and a color that silently does nothing in the sink is worse than no
142
+ // function at all.
143
+ const MIX_PCT_MAX = 100;
144
+ const MIX_RANGE = { min: 0, max: MIX_PCT_MAX, note: "0 = from, 100 = toward" };
145
+ const mixFunc = colorFunc(["string", "string", "float"], ((fromHex, towardHex, pct) => blendRgb(asColor(fromHex, "mix"), asColor(towardHex, "mix"), asAmount(pct, "mix", "percentage", MIX_RANGE) / MIX_PCT_MAX).hex));
146
+ // --- Readability ---
147
+ //
148
+ // Two different questions, so two functions rather than one with a flag:
149
+ // `contrastOn` asks "what is maximally readable here" (black or white, WCAG
150
+ // relative-luminance cutoff); `readableOn` asks "keep this color recognizably
151
+ // itself, but guarantee it clears `ratio`" — it slides OKLCH lightness and
152
+ // preserves hue, so a blue on dark blue becomes a lighter blue rather than
153
+ // white.
154
+ //
155
+ // [LAW:no-silent-failure] `ratio` is required, not defaulted to WCAG AA's 4.5.
156
+ // The threshold is the whole decision this function makes, and the right value
157
+ // genuinely differs by intent — 4.5 for body text, 3.0 for large or
158
+ // deliberately recessive text. A hidden default would silently govern an
159
+ // accessibility outcome the author never saw, and would quietly over-correct a
160
+ // caller whose *point* was to be quiet: de-emphasized text floored at 4.5 is no
161
+ // longer de-emphasized.
162
+ // WCAG 2.x contrast ratios live in [1, 21] — 1 is a color on itself.
163
+ const RATIO_RANGE = {
164
+ min: 1,
165
+ max: 21,
166
+ note: "4.5 = AA body text, 3 = AA large text",
167
+ };
168
+ const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
169
+ const TRUECOLOR = { depth: ColorDepth.TRUECOLOR };
170
+ /**
171
+ * The `readableOn` binding, measuring where `drawnAt` says the text lands when
172
+ * it is evaluated — a consumer that renders to a per-call destination
173
+ * registers its own.
174
+ */
175
+ export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => {
176
+ const { depth, terminal } = drawnAt();
177
+ return ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), depth, undefined, terminal).hex;
178
+ }));
179
+ // --- OKLCH axes ---
180
+ //
181
+ // [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
182
+ // keyed BY the axis so the compiler proves it total: add a field to `ThemeKey`
183
+ // and this object stops satisfying `Record<keyof ThemeKey, string>` until a
184
+ // binding name is chosen for it. The same derive-registrations-from-the-real-
185
+ // inventory trick `attributeFuncs()` uses over `ATTRIBUTE_NAMES`.
186
+ //
187
+ // Naming follows the axis semantics rather than the field spelling: `shift`
188
+ // for the additive axes, `scale` for the multiplicative ones, so a reader can
189
+ // tell from the call site whether `1` means "no change" or "one unit".
190
+ const OKLCH_AXIS_FUNCS = {
191
+ hueShift: "shiftHue",
192
+ chromaScale: "scaleChroma",
193
+ lightnessScale: "scaleLightness",
194
+ lightnessShift: "shiftLightness",
195
+ };
196
+ function oklchAxisFuncs() {
197
+ const out = {};
198
+ for (const [axis, funcName] of Object.entries(OKLCH_AXIS_FUNCS)) {
199
+ // [LAW:dataflow-not-control-flow] Every axis takes the identical path —
200
+ // splice one value into the identity key and apply it. The axis is data.
201
+ const key = axis;
202
+ out[funcName] = colorFunc(["string", "float"], ((hex, amount) => {
203
+ const themeKey = {
204
+ ...IDENTITY,
205
+ [key]: asAmount(amount, funcName, "amount"),
206
+ };
207
+ return Oklch.fromRgba(asColor(hex, funcName)).applyKey(themeKey).toRgba().hex;
208
+ }));
209
+ }
210
+ return out;
211
+ }
212
+ /**
213
+ * The palette-free color vocabulary: every function takes colors and returns a
214
+ * color, so they compose by nesting.
215
+ *
216
+ * | function | meaning |
217
+ * |---|---|
218
+ * | `darken c n` / `lighten c n` | slide HSL lightness by N 10% levels |
219
+ * | `mix a b pct` | blend `a` toward `b` by `pct`% (0 → `a`, 100 → `b`) |
220
+ * | `contrastOn bg` | black or white, whichever is readable on `bg` |
221
+ * | `readableOn fg bg ratio` | `fg` nudged in OKLCH lightness until it clears `ratio` on `bg`, hue preserved |
222
+ * | `shiftHue c deg` | rotate hue in OKLCH |
223
+ * | `scaleChroma c f` | multiply chroma (0 → gray, 1 → identity) |
224
+ * | `scaleLightness c f` | multiply lightness (1 → identity, -1 → invert) |
225
+ * | `shiftLightness c d` | add to lightness, after any scale |
226
+ *
227
+ * Pair with `paletteFuncs()` to name colors from a theme, and with
228
+ * `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
229
+ *
230
+ * @param drawnAt where the text will be drawn — the depth, and the terminal
231
+ * when known — read on every `readableOn` evaluation so a host that learns
232
+ * its destination per render passes one getter; defaults to truecolor.
233
+ */
234
+ export function colorFuncs(drawnAt = () => TRUECOLOR) {
235
+ return {
236
+ darken: darkenFunc,
237
+ lighten: lightenFunc,
238
+ mix: mixFunc,
239
+ contrastOn: contrastOnFunc,
240
+ readableOn: readableOnFunc(drawnAt),
241
+ ...oklchAxisFuncs(),
242
+ };
243
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared internal helpers for the rich-js template binding layer.
3
+ *
4
+ * [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
5
+ * of "apply a Style to an existing RichText fragment." Both the style-function
6
+ * registrations and the palette-function registrations use this same operation;
7
+ * keeping one copy prevents the two from drifting.
8
+ */
9
+ import { type Style } from "../core/style.js";
10
+ import { RichText } from "../core/text.js";
11
+ /**
12
+ * Apply a style on top of an already-styled RichText fragment.
13
+ *
14
+ * [LAW:single-enforcer] Type validation lives here; callers trust the result.
15
+ * The `"liftable"` arg type lifts string literals to `RichText` via the
16
+ * engine's `fromString` before the body runs, so `child` is always a
17
+ * `RichText` by the time this runs. The instanceof check exists because
18
+ * `"liftable"` admits any non-primitive — the engine cannot prove the object
19
+ * is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
20
+ * rather than producing a malformed fragment.
21
+ *
22
+ * Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
23
+ * base style that is a name is layered under, not resolved: a template runs
24
+ * before the render whose theme says what the name stands for.
25
+ */
26
+ export declare function applyStyleToFragment(child: unknown, style: Style): RichText;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared internal helpers for the rich-js template binding layer.
3
+ *
4
+ * [LAW:one-source-of-truth] `applyStyleToFragment` is the single implementation
5
+ * of "apply a Style to an existing RichText fragment." Both the style-function
6
+ * registrations and the palette-function registrations use this same operation;
7
+ * keeping one copy prevents the two from drifting.
8
+ */
9
+ import { layerStyle } from "../core/style.js";
10
+ import { RichText } from "../core/text.js";
11
+ /**
12
+ * Apply a style on top of an already-styled RichText fragment.
13
+ *
14
+ * [LAW:single-enforcer] Type validation lives here; callers trust the result.
15
+ * The `"liftable"` arg type lifts string literals to `RichText` via the
16
+ * engine's `fromString` before the body runs, so `child` is always a
17
+ * `RichText` by the time this runs. The instanceof check exists because
18
+ * `"liftable"` admits any non-primitive — the engine cannot prove the object
19
+ * is the binding's own `T`. Misuse (`{{ red someMap }}`) fails loudly here
20
+ * rather than producing a malformed fragment.
21
+ *
22
+ * Conflict resolution: `Style.add` — the outer (newly applied) style wins. A
23
+ * base style that is a name is layered under, not resolved: a template runs
24
+ * before the render whose theme says what the name stands for.
25
+ */
26
+ export function applyStyleToFragment(child, style) {
27
+ if (!(child instanceof RichText)) {
28
+ throw new TypeError(`template function expected a RichText fragment, got ${typeof child === "object" ? Object.prototype.toString.call(child) : typeof child}`);
29
+ }
30
+ const result = child.copy();
31
+ result.style = layerStyle(child.style, style);
32
+ return result;
33
+ }