@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,86 @@
1
+ /**
2
+ * Rule — a horizontal divider line, optionally with a centered title.
3
+ */
4
+ import { asCellCol, cellLen, setCellSize } from "../core/cells.js";
5
+ import { Segment } from "../core/segment.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { drawable, getStyle } from "../core/protocol.js";
8
+ import { cutLabel, drawLabel, inlineLabel } from "./embed.js";
9
+ const ASCII_RULE_CHAR = "-";
10
+ const DEFAULT_RULE_CHAR = "\u2500"; // ─
11
+ export class Rule {
12
+ title;
13
+ characters;
14
+ align;
15
+ style;
16
+ _label;
17
+ constructor(title, options) {
18
+ const chars = options?.characters ?? DEFAULT_RULE_CHAR;
19
+ // [LAW:parse-dont-validate] Rich's rule: a rule drawn from zero-width
20
+ // characters would advance no cells, so `repeatToWidth` never meets one.
21
+ if (cellLen(chars) < 1) {
22
+ throw new Error("Rule characters must have a cell width of at least 1");
23
+ }
24
+ const align = options?.align;
25
+ if (align !== undefined && align !== "left" && align !== "center" && align !== "right") {
26
+ throw new Error(`Invalid align value: "${align}"`);
27
+ }
28
+ this.title = title;
29
+ this._label = inlineLabel(title);
30
+ this.characters = chars;
31
+ this.align = align ?? "center";
32
+ this.style = options?.style ?? NULL_STYLE;
33
+ }
34
+ *render(options) {
35
+ const maxWidth = options.maxWidth;
36
+ const ruleChar = drawable(options, this.characters, ASCII_RULE_CHAR);
37
+ const style = getStyle(options, this.style);
38
+ const ruleStyle = style.isNull ? undefined : style;
39
+ const line = (width) => new Segment(repeatToWidth(ruleChar, width), ruleStyle);
40
+ // Rich's `required_space`: a centred title keeps a rule cell and a space
41
+ // either side of it, an aligned one a space and a rule cell on its open
42
+ // side. A rule with no room past that draws no title, as one without a title.
43
+ const room = Math.max(0, maxWidth - (this.align === "center" ? 4 : 2));
44
+ if (this._label === undefined || room === 0) {
45
+ yield line(maxWidth);
46
+ yield Segment.line();
47
+ return;
48
+ }
49
+ const text = this._label.text(options);
50
+ cutLabel(text, room, drawable(options, "\u2026", "."));
51
+ const title = drawLabel(text, options, ruleStyle);
52
+ const titleWidth = Segment.getLineLength(title);
53
+ // As Rich lays the line out: the gaps of a centred title are the rule's,
54
+ // styled with it; an aligned title's one gap is unstyled.
55
+ switch (this.align) {
56
+ case "center": {
57
+ const left = Math.floor((maxWidth - titleWidth) / 2) - 1;
58
+ yield line(left);
59
+ yield new Segment(" ", ruleStyle);
60
+ yield* title;
61
+ yield new Segment(" ", ruleStyle);
62
+ yield line(maxWidth - left - titleWidth - 2);
63
+ break;
64
+ }
65
+ case "left":
66
+ yield* title;
67
+ yield new Segment(" ");
68
+ yield line(maxWidth - titleWidth - 1);
69
+ break;
70
+ case "right":
71
+ yield line(maxWidth - titleWidth - 1);
72
+ yield new Segment(" ");
73
+ yield* title;
74
+ break;
75
+ }
76
+ yield Segment.line();
77
+ }
78
+ measure(_options) {
79
+ return { minimum: 1, maximum: _options.maxWidth };
80
+ }
81
+ }
82
+ // [LAW:one-source-of-truth] `setCellSize` crops between the grapheme clusters
83
+ // `cellLen` measures, so `❤️` or `👨‍👩‍👧` is never cut in half or over-counted.
84
+ function repeatToWidth(char, width) {
85
+ return setCellSize(char.repeat(Math.ceil(width / cellLen(char))), asCellCol(width));
86
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Spinner — animated terminal spinner with optional text label.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import { RichText } from "../core/text.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export interface SpinnerOptions {
9
+ speed?: number;
10
+ style?: string | Style;
11
+ }
12
+ export declare class Spinner implements Renderable, Measurable {
13
+ readonly name: string;
14
+ readonly speed: number;
15
+ readonly style: string | Style;
16
+ /**
17
+ * The label drawn after the frame. It is read through `embed`'s one crossing
18
+ * each time it is drawn, so a string is markup under the console that draws
19
+ * it, as a string handed to `print` is — where Rich's `Spinner` parses it
20
+ * whatever the console says — and a `RichText` changed in place draws as
21
+ * changed, as the `Text` Rich keeps does. [LAW:single-enforcer]
22
+ */
23
+ text: string | RichText;
24
+ private readonly _data;
25
+ private _frameIndex;
26
+ private _lastUpdate;
27
+ constructor(name?: string, text?: string | RichText, options?: SpinnerOptions);
28
+ get frames(): readonly string[];
29
+ get interval(): number;
30
+ /** Advance the frame count by the time elapsed, and return it. */
31
+ private _advance;
32
+ /** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
33
+ private _drawnFrames;
34
+ /**
35
+ * The current frame and its label as one line wrapped to the width and
36
+ * ended, as the reference's `Text` is — printed, stacked in a `Group`, or
37
+ * laid out in a `Progress` cell alike.
38
+ */
39
+ render(options: RenderOptions): Iterable<Segment>;
40
+ private _currentLine;
41
+ /**
42
+ * `frame` and the label as one text, as Rich's `Text.assemble(frame, " ",
43
+ * text)`: a long label wraps as one line with its frame. A label that draws
44
+ * nothing — the default `""`, or markup that styles nothing — is no label,
45
+ * not a trailing space, as an empty `Text` is falsy there.
46
+ */
47
+ private _line;
48
+ /**
49
+ * Measured at its widest frame, so a column holding it never narrows as it
50
+ * turns, and never offered less than that frame: a frame is one picture,
51
+ * which the reference's `Text` measure would let wrap apart at its spaces.
52
+ */
53
+ measure(options: RenderOptions): {
54
+ minimum: number;
55
+ maximum: number;
56
+ };
57
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Spinner — animated terminal spinner with optional text label.
3
+ */
4
+ import { cellLen } from "../core/cells.js";
5
+ import { Segment } from "../core/segment.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { RichText } from "../core/text.js";
8
+ import { SPINNERS, DEFAULT_SPINNER, LINE_SPINNER } from "../core/spinnerData.js";
9
+ import { drawable } from "../core/protocol.js";
10
+ import { EmbeddedText } from "./embed.js";
11
+ const spinnerGlyphs = ({ frames }) => frames.join("");
12
+ export class Spinner {
13
+ name;
14
+ speed;
15
+ style;
16
+ /**
17
+ * The label drawn after the frame. It is read through `embed`'s one crossing
18
+ * each time it is drawn, so a string is markup under the console that draws
19
+ * it, as a string handed to `print` is — where Rich's `Spinner` parses it
20
+ * whatever the console says — and a `RichText` changed in place draws as
21
+ * changed, as the `Text` Rich keeps does. [LAW:single-enforcer]
22
+ */
23
+ text;
24
+ _data;
25
+ _frameIndex;
26
+ _lastUpdate;
27
+ constructor(name, text = "", options) {
28
+ const spinnerName = name ?? DEFAULT_SPINNER;
29
+ const data = SPINNERS[spinnerName];
30
+ if (!data) {
31
+ throw new Error(`Unknown spinner: "${spinnerName}"`);
32
+ }
33
+ this.name = spinnerName;
34
+ this.text = text;
35
+ this.speed = options?.speed ?? 1;
36
+ this.style = options?.style ?? NULL_STYLE;
37
+ this._data = data;
38
+ this._frameIndex = 0;
39
+ this._lastUpdate = Date.now();
40
+ }
41
+ get frames() {
42
+ return this._data.frames;
43
+ }
44
+ get interval() {
45
+ return this._data.interval;
46
+ }
47
+ /** Advance the frame count by the time elapsed, and return it. */
48
+ _advance() {
49
+ const now = Date.now();
50
+ // Whole frames passed at this speed; a negative speed steps backwards, as
51
+ // Rich's does, so the index wraps with a modulo that is never negative.
52
+ const steps = Math.trunc(((now - this._lastUpdate) * this.speed) / this.interval);
53
+ if (steps !== 0) {
54
+ const count = this._data.frames.length;
55
+ this._frameIndex = (((this._frameIndex + steps) % count) + count) % count;
56
+ this._lastUpdate = now;
57
+ }
58
+ return this._frameIndex;
59
+ }
60
+ /** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
61
+ _drawnFrames(options) {
62
+ return drawable(options, this._data, LINE_SPINNER, spinnerGlyphs).frames;
63
+ }
64
+ /**
65
+ * The current frame and its label as one line wrapped to the width and
66
+ * ended, as the reference's `Text` is — printed, stacked in a `Group`, or
67
+ * laid out in a `Progress` cell alike.
68
+ */
69
+ *render(options) {
70
+ yield* this._currentLine(options).render(options);
71
+ yield Segment.line();
72
+ }
73
+ _currentLine(options) {
74
+ const frames = this._drawnFrames(options);
75
+ return this._line(frames[this._advance() % frames.length], options);
76
+ }
77
+ /**
78
+ * `frame` and the label as one text, as Rich's `Text.assemble(frame, " ",
79
+ * text)`: a long label wraps as one line with its frame. A label that draws
80
+ * nothing — the default `""`, or markup that styles nothing — is no label,
81
+ * not a trailing space, as an empty `Text` is falsy there.
82
+ */
83
+ _line(frame, options) {
84
+ const line = new RichText("", { end: "" }).append(frame, this.style);
85
+ // Unhighlighted: Rich's label is a `Text` from the moment it is handed over, and a `Text` is never highlighted.
86
+ const label = new EmbeddedText(this.text).text({ ...options, highlight: false });
87
+ if (label.plain !== "")
88
+ line.append(" ").append(label);
89
+ return line;
90
+ }
91
+ /**
92
+ * Measured at its widest frame, so a column holding it never narrows as it
93
+ * turns, and never offered less than that frame: a frame is one picture,
94
+ * which the reference's `Text` measure would let wrap apart at its spaces.
95
+ */
96
+ measure(options) {
97
+ const frames = this._drawnFrames(options);
98
+ const widths = frames.map((frame) => cellLen(frame));
99
+ const frameWidth = Math.max(...widths);
100
+ const { minimum, maximum } = this._line(frames[widths.indexOf(frameWidth)], options).measure(options);
101
+ return { minimum: Math.max(minimum, Math.min(frameWidth, maximum)), maximum };
102
+ }
103
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Status — displays a spinner animation with a status message.
3
+ *
4
+ * Its renderable is a `Spinner` whose label is the message, as Rich's is, so
5
+ * the message is drawn the way the spinner draws its label: a string is markup
6
+ * under the console that draws it.
7
+ */
8
+ import type { Style } from "../core/style.js";
9
+ import type { RichText } from "../core/text.js";
10
+ import { Console } from "../core/console.js";
11
+ export interface StatusOptions {
12
+ spinner?: string;
13
+ speed?: number;
14
+ /** The spinner's frame style; the message styles itself with its markup. */
15
+ spinnerStyle?: string | Style;
16
+ console?: Console;
17
+ }
18
+ export declare class Status {
19
+ private _live;
20
+ private _spinner;
21
+ private _console;
22
+ constructor(message: string | RichText, options?: StatusOptions);
23
+ get console(): Console;
24
+ get message(): string | RichText;
25
+ set message(value: string | RichText);
26
+ start(): void;
27
+ stop(): void;
28
+ update(message: string | RichText): void;
29
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Status — displays a spinner animation with a status message.
3
+ *
4
+ * Its renderable is a `Spinner` whose label is the message, as Rich's is, so
5
+ * the message is drawn the way the spinner draws its label: a string is markup
6
+ * under the console that draws it.
7
+ */
8
+ import { Console } from "../core/console.js";
9
+ import { Spinner } from "./spinner.js";
10
+ import { Live } from "./live.js";
11
+ export class Status {
12
+ _live;
13
+ _spinner;
14
+ _console;
15
+ constructor(message, options) {
16
+ this._console = options?.console ?? new Console();
17
+ this._spinner = new Spinner(options?.spinner ?? "dots", message, {
18
+ speed: options?.speed,
19
+ style: options?.spinnerStyle ?? "status.spinner",
20
+ });
21
+ this._live = new Live(this._spinner, {
22
+ console: this._console,
23
+ transient: true,
24
+ refreshPerSecond: 12.5,
25
+ });
26
+ }
27
+ get console() {
28
+ return this._console;
29
+ }
30
+ get message() {
31
+ return this._spinner.text;
32
+ }
33
+ set message(value) {
34
+ this._spinner.text = value;
35
+ }
36
+ start() {
37
+ this._live.start();
38
+ }
39
+ stop() {
40
+ this._live.stop();
41
+ }
42
+ update(message) {
43
+ this._spinner.text = message;
44
+ this._live.update(this._spinner, { refresh: true });
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Syntax — renders source code with basic syntax highlighting.
3
+ * Uses built-in tokenization (no external dependency).
4
+ *
5
+ * Colours are names in the console's theme (`syntax.keyword`,
6
+ * `syntax.line_number`, …), not a per-instance palette: a theme is how every
7
+ * other renderable here is restyled, and a second styling vocabulary on this
8
+ * one class would be a second answer to "what colour is a keyword".
9
+ */
10
+ import { Segment } from "../core/segment.js";
11
+ import { type Measurable, type Renderable, type RenderOptions } from "../core/protocol.js";
12
+ export interface SyntaxOptions {
13
+ lineNumbers?: boolean;
14
+ startLine?: number;
15
+ lineRange?: [number, number];
16
+ highlightLines?: Set<number>;
17
+ wordWrap?: boolean;
18
+ tabSize?: number;
19
+ }
20
+ /**
21
+ * [LAW:one-type-per-behavior] A language is a value here — a grammar — not a
22
+ * code path. The union is the set of languages this module can actually
23
+ * tokenize, so a name it cannot is a compile error rather than a silent plain
24
+ * render; `"text"` is the grammar with no rules.
25
+ */
26
+ declare const GRAMMARS: {
27
+ javascript: RegExp;
28
+ typescript: RegExp;
29
+ python: RegExp;
30
+ bash: RegExp;
31
+ text: RegExp;
32
+ };
33
+ export type SyntaxLanguage = keyof typeof GRAMMARS;
34
+ export declare class Syntax implements Renderable, Measurable {
35
+ readonly code: string;
36
+ readonly language: SyntaxLanguage;
37
+ readonly lineNumbers: boolean;
38
+ readonly startLine: number;
39
+ readonly lineRange: [number, number] | undefined;
40
+ readonly highlightLines: Set<number>;
41
+ readonly wordWrap: boolean;
42
+ readonly tabSize: number;
43
+ /** The code with its tabs expanded, as lines — what both render and measure read. */
44
+ private readonly _lines;
45
+ constructor(code: string, language?: SyntaxLanguage, options?: SyntaxOptions);
46
+ /**
47
+ * The lines shown, as a 0-based half-open slice of `_lines`, and the number
48
+ * the first is given — render and measure both read them here, so a range
49
+ * counts the same in each. `lineRange` is 1-based and inclusive, and a line
50
+ * keeps the number it has in the whole code: `[3, 4]` shows lines 3 and 4
51
+ * numbered 3 and 4 (with a `startLine` of 1), as Rich does. A range reaching
52
+ * outside the code shows the part inside it, which may be none.
53
+ */
54
+ private _shown;
55
+ /** Digits of the last number, a space, and the `│ ` rule; nothing without numbers. */
56
+ private _gutterWidth;
57
+ render(options: RenderOptions): Iterable<Segment>;
58
+ private _rows;
59
+ /** The gutter is the narrowest it can be drawn; the widest shown line plus the gutter, the widest. */
60
+ measure(options: RenderOptions): {
61
+ minimum: number;
62
+ maximum: number;
63
+ };
64
+ }
65
+ export {};
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Syntax — renders source code with basic syntax highlighting.
3
+ * Uses built-in tokenization (no external dependency).
4
+ *
5
+ * Colours are names in the console's theme (`syntax.keyword`,
6
+ * `syntax.line_number`, …), not a per-instance palette: a theme is how every
7
+ * other renderable here is restyled, and a second styling vocabulary on this
8
+ * one class would be a second answer to "what colour is a keyword".
9
+ */
10
+ import { cellLen, expandTabs, parseTabSize } from "../core/cells.js";
11
+ import { Segment } from "../core/segment.js";
12
+ import { RichText } from "../core/text.js";
13
+ import { drawable, groundedStyle, getStyle, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
14
+ /**
15
+ * One regex per grammar, each token a named alternative. A single left-to-right
16
+ * scan is what keeps a `//` inside a string from starting a comment, and a
17
+ * keyword inside a comment from being drawn as one: at any position the
18
+ * leftmost match wins, and among matches starting there the earliest rule.
19
+ * `(?!)` never matches, so a grammar with no rules is still a regex.
20
+ */
21
+ function grammar(rules) {
22
+ return new RegExp([...rules.map(([token, source]) => `(?<${token}>${source})`), "(?!)"].join("|"), "g");
23
+ }
24
+ // Not after a `.`: `map.get` and `node.type` are properties, not keywords.
25
+ const words = (list) => `(?<!\\.)\\b(?:${list.split(" ").join("|")})\\b`;
26
+ const NUMBER = String.raw `\b(?:0x[0-9a-fA-F]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b`;
27
+ const QUOTED = String.raw `"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'`;
28
+ const JS_KEYWORDS = "function class const let var return if else for while do switch case break continue new throw try catch " +
29
+ "finally import export from default async await yield of in typeof instanceof void delete this super extends static";
30
+ const TS_KEYWORDS = "interface type enum implements private public protected readonly as declare namespace keyof satisfies abstract is infer";
31
+ const JAVASCRIPT = [
32
+ ["comment", String.raw `\/\/[^\n]*|\/\*[\s\S]*?\*\/`],
33
+ ["string", String.raw `${QUOTED}|` + "`(?:[^`\\\\]|\\\\.)*`"],
34
+ ["constant", words("true false null undefined NaN Infinity")],
35
+ ["keyword", words(JS_KEYWORDS)],
36
+ ["number", NUMBER],
37
+ ];
38
+ /**
39
+ * [LAW:one-type-per-behavior] A language is a value here — a grammar — not a
40
+ * code path. The union is the set of languages this module can actually
41
+ * tokenize, so a name it cannot is a compile error rather than a silent plain
42
+ * render; `"text"` is the grammar with no rules.
43
+ */
44
+ const GRAMMARS = {
45
+ javascript: grammar(JAVASCRIPT),
46
+ typescript: grammar(JAVASCRIPT.map(([token, source]) => token === "keyword" ? [token, words(`${JS_KEYWORDS} ${TS_KEYWORDS}`)] : [token, source])),
47
+ python: grammar([
48
+ ["comment", "#[^\\n]*"],
49
+ ["string", String.raw `"""[\s\S]*?"""|'''[\s\S]*?'''|${QUOTED}`],
50
+ ["constant", words("True False None")],
51
+ ["keyword", words("def class return if elif else for while in not and or is import from as with try except finally raise " +
52
+ "pass break continue lambda yield global nonlocal assert del async await self cls")],
53
+ ["number", NUMBER],
54
+ ]),
55
+ bash: grammar([
56
+ ["comment", "(?<![^\\s;])#[^\\n]*"],
57
+ ["string", QUOTED],
58
+ ["keyword", words("if then else elif fi for while until do done case esac function in return local export select")],
59
+ ["number", NUMBER],
60
+ ]),
61
+ text: grammar([]),
62
+ };
63
+ export class Syntax {
64
+ code;
65
+ language;
66
+ lineNumbers;
67
+ startLine;
68
+ lineRange;
69
+ highlightLines;
70
+ wordWrap;
71
+ tabSize;
72
+ /** The code with its tabs expanded, as lines — what both render and measure read. */
73
+ _lines;
74
+ constructor(code, language = "text", options) {
75
+ // A plain-JS caller, or one passing a language read at runtime, gets past
76
+ // the type; left here, the name surfaces at render as a TypeError about
77
+ // `undefined`. [LAW:no-silent-failure] Refused where it is given, by name.
78
+ if (!Object.hasOwn(GRAMMARS, language)) {
79
+ throw new TypeError(`Syntax has no grammar for ${JSON.stringify(language)}: use one of ${Object.keys(GRAMMARS).join(", ")}`);
80
+ }
81
+ this.code = code;
82
+ this.language = language;
83
+ this.lineNumbers = options?.lineNumbers ?? false;
84
+ this.startLine = options?.startLine ?? 1;
85
+ this.lineRange = options?.lineRange;
86
+ this.highlightLines = options?.highlightLines ?? new Set();
87
+ this.wordWrap = options?.wordWrap ?? false;
88
+ const tabSize = parseTabSize(options?.tabSize ?? 4);
89
+ this.tabSize = tabSize;
90
+ this._lines = expandTabs(code, tabSize).text.split("\n");
91
+ }
92
+ /**
93
+ * The lines shown, as a 0-based half-open slice of `_lines`, and the number
94
+ * the first is given — render and measure both read them here, so a range
95
+ * counts the same in each. `lineRange` is 1-based and inclusive, and a line
96
+ * keeps the number it has in the whole code: `[3, 4]` shows lines 3 and 4
97
+ * numbered 3 and 4 (with a `startLine` of 1), as Rich does. A range reaching
98
+ * outside the code shows the part inside it, which may be none.
99
+ */
100
+ _shown() {
101
+ const [start, end] = this.lineRange ?? [1, this._lines.length];
102
+ const from = Math.max(0, start - 1);
103
+ return { first: this.startLine + from, from, to: Math.max(from, end) };
104
+ }
105
+ /** Digits of the last number, a space, and the `│ ` rule; nothing without numbers. */
106
+ _gutterWidth(first, count) {
107
+ return this.lineNumbers ? String(first + count - 1).length + 3 : 0;
108
+ }
109
+ *render(options) {
110
+ const bounded = withBoundedWidth(options, this);
111
+ // A gutter wider than the offer is cut at its edge like any other row.
112
+ yield* Segment.cropLines(this._rows(bounded), bounded.maxWidth);
113
+ }
114
+ *_rows(options) {
115
+ const { first, from, to } = this._shown();
116
+ const numberStyle = getStyle(options, "syntax.line_number");
117
+ const highlightStyle = groundedStyle(options, getStyle(options, "syntax.line_number.highlight"));
118
+ const rule = drawable(options, "│ ", "| ");
119
+ // Tokenized whole and then sliced, so a block comment or a triple-quoted
120
+ // string that spans lines is one token even when the range starts inside it.
121
+ const text = new RichText(this._lines.join("\n"), { end: "" });
122
+ for (const match of text.plain.matchAll(GRAMMARS[this.language])) {
123
+ const [token] = Object.entries(match.groups).find(([, value]) => value !== undefined);
124
+ text.stylize(`syntax.${token}`, match.index, match.index + match[0].length);
125
+ }
126
+ const textLines = text.split("\n").slice(from, to);
127
+ const gutterWidth = this._gutterWidth(first, textLines.length);
128
+ // [LAW:dataflow-not-control-flow] `wordWrap` reaches the line as the
129
+ // overflow it is drawn with, not as a second path: wrapped, a line may
130
+ // become several rows; cropped, it is always one. Never justified: a
131
+ // caller's `justify` would align each line alone and lose its indent.
132
+ const lineOptions = {
133
+ ...options,
134
+ maxWidth: Math.max(0, options.maxWidth - gutterWidth),
135
+ justify: undefined,
136
+ noWrap: !this.wordWrap,
137
+ overflow: this.wordWrap ? "fold" : "crop",
138
+ };
139
+ for (let i = 0; i < textLines.length; i++) {
140
+ const lineNo = first + i;
141
+ const style = this.highlightLines.has(lineNo) ? highlightStyle : numberStyle;
142
+ // Terminated, so an empty line is one empty row rather than none.
143
+ const rows = Segment.splitLines([...textLines[i].render(lineOptions), Segment.line()]);
144
+ for (let row = 0; row < rows.length; row++) {
145
+ if (this.lineNumbers) {
146
+ // A wrapped line's continuation rows keep the gutter, unnumbered.
147
+ const label = row === 0 ? String(lineNo) : "";
148
+ yield new Segment(label.padStart(gutterWidth - 3) + " ", style);
149
+ yield new Segment(rule, numberStyle);
150
+ }
151
+ yield* rows[row];
152
+ yield Segment.line();
153
+ }
154
+ }
155
+ }
156
+ /** The gutter is the narrowest it can be drawn; the widest shown line plus the gutter, the widest. */
157
+ measure(options) {
158
+ const { maxWidth } = withCellWidth(options);
159
+ const { first, from, to } = this._shown();
160
+ const lines = this._lines.slice(from, to);
161
+ const gutterWidth = this._gutterWidth(first, lines.length);
162
+ const widest = lines.reduce((width, line) => Math.max(width, cellLen(line)), 0);
163
+ const maximum = Math.min(gutterWidth + widest, maxWidth);
164
+ return { minimum: Math.min(gutterWidth, maximum), maximum };
165
+ }
166
+ }