@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,71 @@
1
+ /**
2
+ * Prompt — interactive prompts for user input, Rich's `rich/prompt.py`.
3
+ *
4
+ * [LAW:locality-or-seam] The renderable owns prompt logic (display, parsing the
5
+ * answer, choice checking, default fallback, the retry loop and the message
6
+ * each rejected answer prints) — but not where the answer comes from. The
7
+ * input source is a required `PromptInput` capability passed at the call site.
8
+ * Node consumers pass `nodeAsk` from `@promptctl/rich-js/node/prompt`; tests
9
+ * pass a fake; the browser bundle gets the classes without dragging
10
+ * `node:readline` into the main barrel.
11
+ *
12
+ * [LAW:types-are-the-program] The `input: PromptInput` parameter is
13
+ * positional and required on every `*.ask()` static — not an optional in
14
+ * `PromptOptions`. The previous shape allowed `Prompt.ask("name?")` at the
15
+ * type level and threw at runtime; the new shape makes the missing-capability
16
+ * state unrepresentable to TS callers.
17
+ *
18
+ * A trust-boundary `typeof === "function"` check remains because the public
19
+ * API surface is reachable from JS (no compile-time types) and from
20
+ * `any`-typed TS callers. The check makes the failure *diagnostic* (points
21
+ * the caller at the node helper), not gatekeep-against-bugs — TS users
22
+ * never see it because the type already forbids the bad state.
23
+ */
24
+ import { Console } from "../core/console.js";
25
+ import { RichText } from "../core/text.js";
26
+ /**
27
+ * Input capability: receives the prompt to show — styled text, ending in the
28
+ * `": "` the answer is typed after — and the `Console` the prompt was asked
29
+ * on, and resolves with the raw user response. Implementations decide where
30
+ * the input comes from (stdin readline, network, in-memory queue, etc.); the
31
+ * console is what the prompt is drawn with, as Rich's `Console.input` draws
32
+ * it — its theme, its colours, the target it writes to.
33
+ *
34
+ * [LAW:effects-at-boundaries] The prompt arrives as a `RichText`, not bytes:
35
+ * encoding it is the console's job, and the console arrives with it.
36
+ */
37
+ export type PromptInput = (prompt: RichText, console: Console) => Promise<string>;
38
+ export interface PromptOptions<T> {
39
+ default?: T;
40
+ choices?: readonly string[];
41
+ caseSensitive?: boolean;
42
+ showChoices?: boolean;
43
+ showDefault?: boolean;
44
+ /**
45
+ * The console the prompt is drawn with and its invalid-answer messages are
46
+ * printed on — Rich's `console=`. Default: a new `Console`, made when the
47
+ * prompt is asked.
48
+ */
49
+ console?: Console;
50
+ }
51
+ /**
52
+ * Confirm's options. Its two choices are the yes answer and the no answer, in
53
+ * that order, and an answer matches one in any letter case — Rich's `Confirm`,
54
+ * which takes no `case_sensitive`, but where Rich lowercases only the answer
55
+ * and so can never match a choice spelled with a capital.
56
+ */
57
+ export interface ConfirmOptions extends Omit<PromptOptions<boolean>, "choices" | "caseSensitive"> {
58
+ choices?: readonly [yes: string, no: string];
59
+ }
60
+ export declare class Prompt {
61
+ static ask(promptText: string, input: PromptInput, options?: PromptOptions<string>): Promise<string>;
62
+ }
63
+ export declare class IntPrompt {
64
+ static ask(promptText: string, input: PromptInput, options?: PromptOptions<number>): Promise<number>;
65
+ }
66
+ export declare class FloatPrompt {
67
+ static ask(promptText: string, input: PromptInput, options?: PromptOptions<number>): Promise<number>;
68
+ }
69
+ export declare class Confirm {
70
+ static ask(promptText: string, input: PromptInput, options?: ConfirmOptions): Promise<boolean>;
71
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Prompt — interactive prompts for user input, Rich's `rich/prompt.py`.
3
+ *
4
+ * [LAW:locality-or-seam] The renderable owns prompt logic (display, parsing the
5
+ * answer, choice checking, default fallback, the retry loop and the message
6
+ * each rejected answer prints) — but not where the answer comes from. The
7
+ * input source is a required `PromptInput` capability passed at the call site.
8
+ * Node consumers pass `nodeAsk` from `@promptctl/rich-js/node/prompt`; tests
9
+ * pass a fake; the browser bundle gets the classes without dragging
10
+ * `node:readline` into the main barrel.
11
+ *
12
+ * [LAW:types-are-the-program] The `input: PromptInput` parameter is
13
+ * positional and required on every `*.ask()` static — not an optional in
14
+ * `PromptOptions`. The previous shape allowed `Prompt.ask("name?")` at the
15
+ * type level and threw at runtime; the new shape makes the missing-capability
16
+ * state unrepresentable to TS callers.
17
+ *
18
+ * A trust-boundary `typeof === "function"` check remains because the public
19
+ * API surface is reachable from JS (no compile-time types) and from
20
+ * `any`-typed TS callers. The check makes the failure *diagnostic* (points
21
+ * the caller at the node helper), not gatekeep-against-bugs — TS users
22
+ * never see it because the type already forbids the bad state.
23
+ */
24
+ import { Console } from "../core/console.js";
25
+ import { renderMarkup } from "../core/markup.js";
26
+ import { RichText } from "../core/text.js";
27
+ /** An answer the prompt refused, and the markup it prints before asking again — Rich's `InvalidResponse`. */
28
+ class InvalidResponse {
29
+ message;
30
+ constructor(message) {
31
+ this.message = message;
32
+ }
33
+ }
34
+ const ILLEGAL_CHOICE = new InvalidResponse("[prompt.invalid.choice]Please select one of the available options");
35
+ // The prompt as Rich's `make_prompt` draws it: the caller's markup, then each
36
+ // hint appended as plain text — never read as markup, so `[y/n]` is drawn and
37
+ // not parsed as a tag — then the suffix.
38
+ function makePrompt(promptText, kind, options) {
39
+ const prompt = new RichText("", { style: "prompt", end: "" }).append(renderMarkup(promptText));
40
+ // Rich's `if self.show_choices and self.choices`: an empty list draws no hint.
41
+ if (options.showChoices !== false && kind.choices !== undefined && kind.choices.length > 0) {
42
+ prompt.append(" ").append(`[${kind.choices.join("/")}]`, "prompt.choices");
43
+ }
44
+ if (options.showDefault !== false && options.default !== undefined) {
45
+ prompt.append(" ").append(`(${kind.renderDefault(options.default)})`, "prompt.default");
46
+ }
47
+ return prompt.append(": ");
48
+ }
49
+ // Rich's `PromptBase.__call__`: ask, return the default for an empty answer —
50
+ // the answer as typed, so one of spaces is processed rather than defaulted —
51
+ // and otherwise process it, printing why it was refused and asking again.
52
+ async function run(promptText, input, kind, options) {
53
+ // [LAW:single-enforcer] Trust-boundary validation for non-TS callers
54
+ // (JS, or TS with `any` laundering). TS callers can't reach this branch
55
+ // because `PromptInput` is required at every static `.ask`. The message
56
+ // points at the node helper rather than letting the call site die with
57
+ // a generic `TypeError: input is not a function`.
58
+ if (typeof input !== "function") {
59
+ throw new TypeError("Prompt: `input` must be a `PromptInput` function. Pass `nodeAsk` from " +
60
+ "`@promptctl/rich-js/node/prompt` for Node, or supply a custom " +
61
+ "`PromptInput` for tests/browsers.");
62
+ }
63
+ const console = options.console ?? new Console();
64
+ const prompt = makePrompt(promptText, kind, options);
65
+ while (true) {
66
+ // A copy, because a `RichText` is mutable and the same prompt is asked
67
+ // again on every retry.
68
+ const answer = await input(prompt.copy(), console);
69
+ if (answer === "" && options.default !== undefined)
70
+ return options.default;
71
+ const value = kind.process(answer);
72
+ if (!(value instanceof InvalidResponse))
73
+ return value;
74
+ // The message is this module's markup, read as markup whatever the
75
+ // console's own `markup` setting.
76
+ console.print(renderMarkup(value.message));
77
+ }
78
+ }
79
+ /**
80
+ * Rich's `PromptBase.process_response`: the stripped answer converted, then
81
+ * checked against the choices — when there are any — and the value of the
82
+ * choice it matched returned, so a case-insensitive match answers with the
83
+ * choice as written. An answer that neither converts nor matches is refused
84
+ * for not converting, the check Rich makes first.
85
+ */
86
+ function responseKind(convert, invalid, options) {
87
+ const choices = options.choices;
88
+ const caseSensitive = options.caseSensitive !== false;
89
+ const matches = (choice, value) => caseSensitive ? choice === value : choice.toLowerCase() === value.toLowerCase();
90
+ return {
91
+ choices,
92
+ renderDefault: String,
93
+ process(answer) {
94
+ const value = answer.trim();
95
+ // [LAW:dataflow-not-control-flow] With no choices every answer is its
96
+ // own choice. A choice matched is the answer but for letter case, which
97
+ // no number grammar here reads, so it converts exactly when the answer does.
98
+ const choice = choices === undefined ? value : choices.find((c) => matches(c, value));
99
+ const converted = convert(choice ?? value);
100
+ if (converted === undefined)
101
+ return invalid;
102
+ return choice === undefined ? ILLEGAL_CHOICE : converted;
103
+ },
104
+ };
105
+ }
106
+ /** Python's `int()` over a string: a sign, then digits, any two of which may have one `_` between them. */
107
+ const INTEGER = /^[+-]?\d(?:_?\d)*$/;
108
+ const DIGITS = String.raw `\d(?:_?\d)*`;
109
+ /** Python's `float()` over a string: a decimal with an optional exponent, or `inf`, `infinity` or `nan`, in any case. */
110
+ const FLOAT = new RegExp(String.raw `^[+-]?(?:(?:${DIGITS}(?:\.(?:${DIGITS})?)?|\.${DIGITS})(?:e[+-]?${DIGITS})?|inf(?:inity)?|nan)$`, "i");
111
+ /**
112
+ * The integer an answer spells, as Python's `int()` reads it, or none. One a
113
+ * `number` cannot hold exactly is none: Python's int is unbounded and a
114
+ * rounded value is not the number typed, so it is refused rather than changed.
115
+ */
116
+ function pythonInt(value) {
117
+ if (!INTEGER.test(value))
118
+ return undefined;
119
+ // `+ 0` folds `-0` into 0: Python has no negative integer zero.
120
+ const integer = Number(value.replaceAll("_", "")) + 0;
121
+ return Number.isSafeInteger(integer) ? integer : undefined;
122
+ }
123
+ /** The number an answer spells, as Python's `float()` reads it, or none. */
124
+ function pythonFloat(value) {
125
+ if (!FLOAT.test(value))
126
+ return undefined;
127
+ return Number(value.replaceAll("_", "").toLowerCase().replace(/inf(inity)?$/, "Infinity"));
128
+ }
129
+ // --- Prompt ---
130
+ export class Prompt {
131
+ static ask(promptText, input, options = {}) {
132
+ // Rich's base message, which a string prompt never prints: every answer converts.
133
+ const kind = responseKind((value) => value, new InvalidResponse("[prompt.invalid]Please enter a valid value"), options);
134
+ return run(promptText, input, kind, options);
135
+ }
136
+ }
137
+ export class IntPrompt {
138
+ static ask(promptText, input, options = {}) {
139
+ const kind = responseKind(pythonInt, new InvalidResponse("[prompt.invalid]Please enter a valid integer number"), options);
140
+ return run(promptText, input, kind, options);
141
+ }
142
+ }
143
+ export class FloatPrompt {
144
+ static ask(promptText, input, options = {}) {
145
+ const kind = responseKind(pythonFloat, new InvalidResponse("[prompt.invalid]Please enter a number"), options);
146
+ return run(promptText, input, kind, options);
147
+ }
148
+ }
149
+ const CONFIRM_INVALID = new InvalidResponse("[prompt.invalid]Please enter Y or N");
150
+ export class Confirm {
151
+ static ask(promptText, input, options = {}) {
152
+ const [yes, no] = options.choices ?? ["y", "n"];
153
+ const kind = {
154
+ choices: [yes, no],
155
+ renderDefault: (value) => (value ? yes : no),
156
+ process(answer) {
157
+ const value = answer.trim().toLowerCase();
158
+ if (value === yes.toLowerCase())
159
+ return true;
160
+ return value === no.toLowerCase() ? false : CONFIRM_INVALID;
161
+ },
162
+ };
163
+ return run(promptText, input, kind, options);
164
+ }
165
+ }
@@ -0,0 +1,123 @@
1
+ import type { CellCol } from "../core/cells.js";
2
+ /**
3
+ * Rich's `_ratio.py`: integer totals split into parts by ratio, in exact
4
+ * arithmetic, each by the reference's own rounding.
5
+ *
6
+ * The rounding is the reference's and is kept for that reason alone. A
7
+ * largest-remainder split is fairer, and it is a different answer: 17 cells
8
+ * over ratios 2 and 1 are 12 + 5 by `ratioDistribute` and 11 + 6 by largest
9
+ * remainder, and 5 cells at 1:1 are 2 + 3 by `ratioResolve` and 3 + 2 by
10
+ * largest remainder with ties to the left. A renderable that splits by any
11
+ * other rule cannot print the bytes Rich prints for the same call.
12
+ *
13
+ * The arithmetic is the reference's too: Python's integers and `Fraction`s are
14
+ * exact at any size, and a double's `ratio * remaining` rounds once it passes
15
+ * 2^53, so the parts stopped summing to the total near
16
+ * `Number.MAX_SAFE_INTEGER`.
17
+ */
18
+ /**
19
+ * Rich's `ratio_distribute`: each part the ceiling of its share of what the
20
+ * parts before it left, and never below its minimum, so the rounding lands on
21
+ * the leftmost parts. Given `minimums`, a part whose minimum is 0 takes no
22
+ * share, as the reference zeroes its ratio first. The parts sum to the total
23
+ * whenever the minimums leave room; a minimum that does not fit is paid anyway,
24
+ * as the reference pays it, and the parts then overrun the total. The ratios
25
+ * are weights in one exact proportion (`exactWeights`), so a fractional one
26
+ * divides as it reads, where the reference takes only whole ones.
27
+ */
28
+ export declare function ratioDistribute(total: number, ratios: readonly number[], minimums?: readonly number[]): number[];
29
+ /**
30
+ * Rich's `ratio_reduce`: `total` taken back from `values` by ratio, each part
31
+ * giving up at most its `maximum`, and a part whose maximum is 0 giving up
32
+ * nothing, as the reference zeroes its ratio first. Each part gives up its
33
+ * share of what the parts before it left, rounded half to even as Python's
34
+ * `round` is, so 9 at 1:1 is 4 + 5. A part held to its maximum leaves the rest
35
+ * to the parts after it; when they cannot take it all, less than `total` is
36
+ * taken back, as in the reference.
37
+ */
38
+ export declare function ratioReduce(total: number, ratios: readonly number[], maximums: readonly number[], values: readonly number[]): number[];
39
+ declare const _weight: unique symbol;
40
+ /** A share weight: positive and finite, or 0 for a part that does not grow. */
41
+ export type Weight = number & {
42
+ readonly [_weight]: true;
43
+ };
44
+ /**
45
+ * A share weight, not a cell count: fractions divide space meaningfully, so
46
+ * this parses where `cellCount` would floor. A weight that cannot name a share
47
+ * — negative, NaN, infinite — reads as zero, which already means "this part
48
+ * does not grow" and is set aside before any division.
49
+ */
50
+ export declare function shareWeight(ratio: number): Weight;
51
+ /**
52
+ * One region a `ratioResolve` total is split into: a declared `size`, or —
53
+ * undefined — a share by `ratio`, never below `minimumSize`. A `ratio` of 0 is
54
+ * a part that does not grow and takes its `minimumSize`.
55
+ */
56
+ export interface Edge {
57
+ readonly size: CellCol | undefined;
58
+ readonly ratio: Weight;
59
+ readonly minimumSize: CellCol;
60
+ }
61
+ /**
62
+ * Rich's `ratio_resolve`: `total` cells split across `edges`, the declared
63
+ * sizes paid first and what is left shared by ratio.
64
+ *
65
+ * A growing edge whose share would not reach its `minimumSize` is paid that
66
+ * minimum and the rest are shared again without it, one edge at a time, first
67
+ * in order first. The shares then round by carrying each fraction into the edge
68
+ * after it, so they sum to what was left and the spare cells land on the
69
+ * rightmost edges: 5 cells at 1:1 are 2 + 3.
70
+ *
71
+ * Three departures, all the port's, not the reference's. Nothing is paid past
72
+ * what `total` still holds: Rich hands every edge its minimum even when that
73
+ * overruns the total and leaves the overflow for the screen to clip; here an
74
+ * edge is paid out of what is left, so the parts never sum past `total` and two
75
+ * panes of a layout asked to fit one cell cannot merge into a two-cell row. And
76
+ * a declared `size` of 0 is a pane of no cells, where Rich's `edge.size or
77
+ * None` reads it as undeclared and lets the pane grow. And a `ratio` of 0 is
78
+ * paid its minimum before any share is cut, where Rich keeps it among the
79
+ * growing parts and counts it as 1 in the sum (`edge.ratio or 1`), which thins
80
+ * every other share and can leave cells unassigned.
81
+ */
82
+ export declare function ratioResolve(total: number, edges: readonly Edge[]): number[];
83
+ /**
84
+ * A `total` at which `ratioResolve` gives every growing edge at least
85
+ * `wants[i]` cells, and every other edge its declared size or minimum.
86
+ *
87
+ * [LAW:one-source-of-truth] It inverts the split above and sits beside it for
88
+ * that reason. A growing edge is never handed less than the floor of its share,
89
+ * `total * ratio / totalRatio`, because the running total it is carved from
90
+ * floors once on each side. So a budget at which every share's floor reaches
91
+ * its want is enough, and one budget serves them all: the row needs the largest
92
+ * such demand, never their total.
93
+ *
94
+ * Enough, and not always the least. Carried rounding hands spare cells to the
95
+ * right, so "left" and "right" at 1:1 fit in 9, as 4 + 5, where this answers
96
+ * 10. The least total is no closed formula's answer — an edge's cells are not
97
+ * monotone in the total — and searching for it costs a split per cell climbed,
98
+ * a climb that grows with how far apart the ratios are. The spare cells are
99
+ * cheaper.
100
+ *
101
+ * At that budget every share already covers its edge's minimum — the want
102
+ * includes it — so the re-share pass pays at most an edge whose share is
103
+ * exactly that minimum, which leaves every other share where it was. A want is
104
+ * whole cells, a fraction rounded up; an unbounded one, a pane with no width of
105
+ * its own offered all of it, is an unbounded budget, given before the exact
106
+ * arithmetic, which has no infinity.
107
+ */
108
+ export declare function ratioBudget(edges: readonly Edge[], wants: readonly number[]): number;
109
+ /**
110
+ * Weights as integers in one exact proportion. Each is read as the decimal it
111
+ * prints as — the number its caller wrote, `0.1` rather than the binary double
112
+ * nearest it — and all are scaled by the one power of ten that makes them
113
+ * whole, so `0.1 : 0.3 : 1` is exactly `1 : 3 : 10`. A weight that does not
114
+ * print as a decimal, `Infinity`, is refused by `BigInt` with a SyntaxError:
115
+ * each caller parses its weights finite first, and this is the loud end of
116
+ * that.
117
+ *
118
+ * The common exponent is accumulated rather than spread into `Math.max`: a
119
+ * layout holds as many panes as its caller made, and one argument per pane
120
+ * overruns the engine's argument limit.
121
+ */
122
+ export declare function exactWeights(weights: readonly number[]): bigint[];
123
+ export {};
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Rich's `_ratio.py`: integer totals split into parts by ratio, in exact
3
+ * arithmetic, each by the reference's own rounding.
4
+ *
5
+ * The rounding is the reference's and is kept for that reason alone. A
6
+ * largest-remainder split is fairer, and it is a different answer: 17 cells
7
+ * over ratios 2 and 1 are 12 + 5 by `ratioDistribute` and 11 + 6 by largest
8
+ * remainder, and 5 cells at 1:1 are 2 + 3 by `ratioResolve` and 3 + 2 by
9
+ * largest remainder with ties to the left. A renderable that splits by any
10
+ * other rule cannot print the bytes Rich prints for the same call.
11
+ *
12
+ * The arithmetic is the reference's too: Python's integers and `Fraction`s are
13
+ * exact at any size, and a double's `ratio * remaining` rounds once it passes
14
+ * 2^53, so the parts stopped summing to the total near
15
+ * `Number.MAX_SAFE_INTEGER`.
16
+ */
17
+ /**
18
+ * Rich's `ratio_distribute`: each part the ceiling of its share of what the
19
+ * parts before it left, and never below its minimum, so the rounding lands on
20
+ * the leftmost parts. Given `minimums`, a part whose minimum is 0 takes no
21
+ * share, as the reference zeroes its ratio first. The parts sum to the total
22
+ * whenever the minimums leave room; a minimum that does not fit is paid anyway,
23
+ * as the reference pays it, and the parts then overrun the total. The ratios
24
+ * are weights in one exact proportion (`exactWeights`), so a fractional one
25
+ * divides as it reads, where the reference takes only whole ones.
26
+ */
27
+ export function ratioDistribute(total, ratios, minimums) {
28
+ const weights = exactWeights(minimums === undefined ? ratios : ratios.map((ratio, index) => (minimums[index] > 0 ? ratio : 0)));
29
+ let totalRatio = weights.reduce((sum, ratio) => sum + ratio, 0n);
30
+ let remaining = BigInt(total);
31
+ return weights.map((ratio, index) => {
32
+ // A zero ratio sum is the reference's `else` arm: whatever is left goes to
33
+ // the part at hand, so the total is still handed out in full.
34
+ const share = totalRatio > 0n ? ceilDiv(ratio * remaining, totalRatio) : remaining;
35
+ const minimum = BigInt(minimums?.[index] ?? 0);
36
+ const part = share > minimum ? share : minimum;
37
+ totalRatio -= ratio;
38
+ remaining -= part;
39
+ return Number(part);
40
+ });
41
+ }
42
+ /**
43
+ * Rich's `ratio_reduce`: `total` taken back from `values` by ratio, each part
44
+ * giving up at most its `maximum`, and a part whose maximum is 0 giving up
45
+ * nothing, as the reference zeroes its ratio first. Each part gives up its
46
+ * share of what the parts before it left, rounded half to even as Python's
47
+ * `round` is, so 9 at 1:1 is 4 + 5. A part held to its maximum leaves the rest
48
+ * to the parts after it; when they cannot take it all, less than `total` is
49
+ * taken back, as in the reference.
50
+ */
51
+ export function ratioReduce(total, ratios, maximums, values) {
52
+ const weights = exactWeights(ratios.map((ratio, index) => (maximums[index] > 0 ? ratio : 0)));
53
+ let totalRatio = weights.reduce((sum, ratio) => sum + ratio, 0n);
54
+ let remaining = BigInt(total);
55
+ return weights.map((ratio, index) => {
56
+ if (ratio === 0n)
57
+ return values[index];
58
+ const share = roundDiv(ratio * remaining, totalRatio);
59
+ const maximum = BigInt(maximums[index]);
60
+ const given = share < maximum ? share : maximum;
61
+ remaining -= given;
62
+ totalRatio -= ratio;
63
+ return values[index] - Number(given);
64
+ });
65
+ }
66
+ /** Python's `round(a / b)` for a positive `b`: the nearest integer, a tie to the even one. */
67
+ function roundDiv(a, b) {
68
+ const floor = a / b - (a % b < 0n ? 1n : 0n);
69
+ const twice = 2n * (a - floor * b);
70
+ return twice > b || (twice === b && floor % 2n !== 0n) ? floor + 1n : floor;
71
+ }
72
+ /** Python's `ceil(a / b)` for a positive `b`: `BigInt` division truncates toward zero, which is the ceiling for a negative `a`. */
73
+ function ceilDiv(a, b) {
74
+ const quotient = a / b;
75
+ return a % b > 0n ? quotient + 1n : quotient;
76
+ }
77
+ /**
78
+ * A share weight, not a cell count: fractions divide space meaningfully, so
79
+ * this parses where `cellCount` would floor. A weight that cannot name a share
80
+ * — negative, NaN, infinite — reads as zero, which already means "this part
81
+ * does not grow" and is set aside before any division.
82
+ */
83
+ export function shareWeight(ratio) {
84
+ return (Number.isFinite(ratio) && ratio > 0 ? ratio : 0);
85
+ }
86
+ /** Whether an edge takes a share by ratio; one that does not is paid `size ?? minimumSize`. */
87
+ const grows = (edge) => edge.size === undefined && edge.ratio !== 0;
88
+ /**
89
+ * Rich's `ratio_resolve`: `total` cells split across `edges`, the declared
90
+ * sizes paid first and what is left shared by ratio.
91
+ *
92
+ * A growing edge whose share would not reach its `minimumSize` is paid that
93
+ * minimum and the rest are shared again without it, one edge at a time, first
94
+ * in order first. The shares then round by carrying each fraction into the edge
95
+ * after it, so they sum to what was left and the spare cells land on the
96
+ * rightmost edges: 5 cells at 1:1 are 2 + 3.
97
+ *
98
+ * Three departures, all the port's, not the reference's. Nothing is paid past
99
+ * what `total` still holds: Rich hands every edge its minimum even when that
100
+ * overruns the total and leaves the overflow for the screen to clip; here an
101
+ * edge is paid out of what is left, so the parts never sum past `total` and two
102
+ * panes of a layout asked to fit one cell cannot merge into a two-cell row. And
103
+ * a declared `size` of 0 is a pane of no cells, where Rich's `edge.size or
104
+ * None` reads it as undeclared and lets the pane grow. And a `ratio` of 0 is
105
+ * paid its minimum before any share is cut, where Rich keeps it among the
106
+ * growing parts and counts it as 1 in the sum (`edge.ratio or 1`), which thins
107
+ * every other share and can leave cells unassigned.
108
+ */
109
+ export function ratioResolve(total, edges) {
110
+ const sizes = edges.map(() => 0);
111
+ let remaining = total;
112
+ const pay = (index, cells) => {
113
+ sizes[index] = Math.min(cells, remaining);
114
+ remaining -= sizes[index];
115
+ };
116
+ const growing = [];
117
+ edges.forEach((edge, index) => {
118
+ if (grows(edge))
119
+ growing.push(index);
120
+ else
121
+ pay(index, edge.size ?? edge.minimumSize);
122
+ });
123
+ const weights = exactWeights(growing.map((index) => edges[index].ratio));
124
+ let open = growing.map((index, slot) => ({
125
+ index,
126
+ weight: weights[slot],
127
+ minimum: BigInt(edges[index].minimumSize),
128
+ }));
129
+ // `remaining * weight / sum` is the edge's share; compared across the
130
+ // division so the test is exact. A budget already spent fails every edge's
131
+ // test, which pays each its minimum out of nothing — zero, the clip above.
132
+ let sum = open.reduce((acc, { weight }) => acc + weight, 0n);
133
+ const firstShort = () => open.find(({ weight, minimum }) => BigInt(remaining) * weight <= minimum * sum);
134
+ for (let short = firstShort(); short !== undefined; short = firstShort()) {
135
+ pay(short.index, Number(short.minimum));
136
+ sum -= short.weight;
137
+ open = open.filter((edge) => edge !== short);
138
+ }
139
+ // Carrying each edge's fraction into the next is the same as flooring the
140
+ // running total of the shares: an edge gets the whole cells its share moves
141
+ // that running total across.
142
+ const budget = BigInt(remaining);
143
+ let weightSoFar = 0n;
144
+ let cellsSoFar = 0n;
145
+ for (const { index, weight } of open) {
146
+ const before = cellsSoFar;
147
+ weightSoFar += weight;
148
+ cellsSoFar = (budget * weightSoFar) / sum;
149
+ sizes[index] = Number(cellsSoFar - before);
150
+ }
151
+ return sizes;
152
+ }
153
+ /**
154
+ * A `total` at which `ratioResolve` gives every growing edge at least
155
+ * `wants[i]` cells, and every other edge its declared size or minimum.
156
+ *
157
+ * [LAW:one-source-of-truth] It inverts the split above and sits beside it for
158
+ * that reason. A growing edge is never handed less than the floor of its share,
159
+ * `total * ratio / totalRatio`, because the running total it is carved from
160
+ * floors once on each side. So a budget at which every share's floor reaches
161
+ * its want is enough, and one budget serves them all: the row needs the largest
162
+ * such demand, never their total.
163
+ *
164
+ * Enough, and not always the least. Carried rounding hands spare cells to the
165
+ * right, so "left" and "right" at 1:1 fit in 9, as 4 + 5, where this answers
166
+ * 10. The least total is no closed formula's answer — an edge's cells are not
167
+ * monotone in the total — and searching for it costs a split per cell climbed,
168
+ * a climb that grows with how far apart the ratios are. The spare cells are
169
+ * cheaper.
170
+ *
171
+ * At that budget every share already covers its edge's minimum — the want
172
+ * includes it — so the re-share pass pays at most an edge whose share is
173
+ * exactly that minimum, which leaves every other share where it was. A want is
174
+ * whole cells, a fraction rounded up; an unbounded one, a pane with no width of
175
+ * its own offered all of it, is an unbounded budget, given before the exact
176
+ * arithmetic, which has no infinity.
177
+ */
178
+ export function ratioBudget(edges, wants) {
179
+ const growing = edges.flatMap((edge, index) => (grows(edge) ? [index] : []));
180
+ const owed = growing.map((index) => Math.ceil(Math.max(edges[index].minimumSize, wants[index])));
181
+ if (!owed.every(Number.isFinite))
182
+ return Infinity;
183
+ const weights = exactWeights(growing.map((index) => edges[index].ratio));
184
+ const sum = weights.reduce((acc, weight) => acc + weight, 0n);
185
+ const pinned = edges.reduce((acc, edge) => acc + (grows(edge) ? 0 : (edge.size ?? edge.minimumSize)), 0);
186
+ const budget = owed.reduce((most, cells, slot) => {
187
+ const need = ceilDiv(BigInt(cells) * sum, weights[slot]);
188
+ return need > most ? need : most;
189
+ }, 0n);
190
+ return pinned + Number(budget);
191
+ }
192
+ /**
193
+ * Weights as integers in one exact proportion. Each is read as the decimal it
194
+ * prints as — the number its caller wrote, `0.1` rather than the binary double
195
+ * nearest it — and all are scaled by the one power of ten that makes them
196
+ * whole, so `0.1 : 0.3 : 1` is exactly `1 : 3 : 10`. A weight that does not
197
+ * print as a decimal, `Infinity`, is refused by `BigInt` with a SyntaxError:
198
+ * each caller parses its weights finite first, and this is the loud end of
199
+ * that.
200
+ *
201
+ * The common exponent is accumulated rather than spread into `Math.max`: a
202
+ * layout holds as many panes as its caller made, and one argument per pane
203
+ * overruns the engine's argument limit.
204
+ */
205
+ export function exactWeights(weights) {
206
+ const decimals = weights.map((weight) => {
207
+ const [digits = "", power = "0"] = String(weight).split("e");
208
+ const [whole = "", fraction = ""] = digits.split(".");
209
+ return { mantissa: BigInt(whole + fraction), exponent: fraction.length - Number(power) };
210
+ });
211
+ const common = decimals.reduce((most, { exponent }) => Math.max(most, exponent), 0);
212
+ return decimals.map(({ mantissa, exponent }) => mantissa * 10n ** BigInt(common - exponent));
213
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Rule — a horizontal divider line, optionally with a centered title.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import type { RichText } from "../core/text.js";
6
+ import { Style } from "../core/style.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
+ export type RuleAlign = "left" | "center" | "right";
9
+ export interface RuleOptions {
10
+ characters?: string;
11
+ align?: RuleAlign;
12
+ style?: string | Style;
13
+ }
14
+ export declare class Rule implements Renderable, Measurable {
15
+ readonly title: string | RichText | undefined;
16
+ readonly characters: string;
17
+ readonly align: RuleAlign;
18
+ readonly style: string | Style;
19
+ private readonly _label;
20
+ constructor(title?: string | RichText, options?: RuleOptions);
21
+ render(options: RenderOptions): Iterable<Segment>;
22
+ measure(_options: RenderOptions): {
23
+ minimum: number;
24
+ maximum: number;
25
+ };
26
+ }