@promptctl/rich-js 0.18.1 → 0.20.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.
- package/README.md +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ProgressBar — a visual progress bar rendered with block characters.
|
|
3
3
|
*/
|
|
4
|
+
import { cellCount } from "../core/cells.js";
|
|
5
|
+
import { ColorDepth, ColorRgba, ColorSpec, resolveTerminal } from "../core/color.js";
|
|
4
6
|
import { Segment } from "../core/segment.js";
|
|
5
|
-
import {
|
|
7
|
+
import { Style } from "../core/style.js";
|
|
6
8
|
import { drawable, getStyle } from "../core/protocol.js";
|
|
9
|
+
import { Effected } from "./effect.js";
|
|
10
|
+
import { EFFECT_CURVES, onColors, shimmer } from "./effects.js";
|
|
7
11
|
const BAR = "━";
|
|
8
12
|
const ASCII_BAR = "-";
|
|
13
|
+
const HALF_BAR_RIGHT = "╸";
|
|
14
|
+
const HALF_BAR_LEFT = "╺";
|
|
15
|
+
/**
|
|
16
|
+
* The pulse's shimmer: the signed-off ease and swing, at a period that carries
|
|
17
|
+
* the light along a bar at about Rich's pace, and a band reaching half Rich's
|
|
18
|
+
* `PULSE_SIZE` of 20 cells either side of its centre.
|
|
19
|
+
*/
|
|
20
|
+
const PULSE_CURVE = { ...EFFECT_CURVES.shimmer, seconds: 5 };
|
|
21
|
+
const PULSE_REACH = 10;
|
|
22
|
+
/** Rich's pulse back where the back style names no colour. */
|
|
23
|
+
const PULSE_BACK = ColorSpec.fromRgba(new ColorRgba(0, 0, 0));
|
|
9
24
|
export class ProgressBar {
|
|
10
25
|
total;
|
|
11
26
|
completed;
|
|
@@ -18,36 +33,76 @@ export class ProgressBar {
|
|
|
18
33
|
this.total = options?.total ?? 100;
|
|
19
34
|
this.completed = options?.completed ?? 0;
|
|
20
35
|
this.width = options?.width;
|
|
21
|
-
this.pulse = options?.pulse
|
|
22
|
-
this.style = options?.style ??
|
|
36
|
+
this.pulse = options?.pulse;
|
|
37
|
+
this.style = options?.style ?? "bar.back";
|
|
23
38
|
this.completeStyle = options?.completeStyle ?? "bar.complete";
|
|
24
39
|
this.finishedStyle = options?.finishedStyle ?? "bar.finished";
|
|
25
40
|
}
|
|
26
|
-
get percentComplete() {
|
|
27
|
-
if (this.total <= 0)
|
|
28
|
-
return 0;
|
|
29
|
-
return Math.min(1, Math.max(0, this.completed / this.total));
|
|
30
|
-
}
|
|
31
41
|
*render(options) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
// Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
|
|
43
|
+
// [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
|
|
44
|
+
const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
|
|
45
|
+
if (this.pulse === undefined) {
|
|
46
|
+
yield* this.track(options, width, this.completeHalves(width), getStyle(options, this.style));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
|
|
50
|
+
// Rich's pulse resolves its colours on the terminal a depth assumes, as
|
|
51
|
+
// `get_truecolor` does with no theme; so does this.
|
|
52
|
+
const theme = resolveTerminal(undefined, depth);
|
|
53
|
+
// Rich writes the pulse's back as a colour of its own, black where the
|
|
54
|
+
// back style names none, so the light always has the bar's colour to fall
|
|
55
|
+
// on: one the terminal supplies takes no light.
|
|
56
|
+
const style = getStyle(options, this.style);
|
|
57
|
+
const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(theme, true));
|
|
58
|
+
const back = style.add(Style.fromColor(ink));
|
|
59
|
+
// [LAW:no-ambient-temporal-coupling] The moment is the caller's; the bar
|
|
60
|
+
// only says what it looks like then. An empty track, lit by the shimmer.
|
|
61
|
+
const track = { render: (o) => this.track(o, width, 0, back) };
|
|
62
|
+
yield* new Effected(track, this.pulseEffect(options, back, width, depth, theme), { t: this.pulse.t, key: "progress-bar", theme }).render(options);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
66
|
+
* `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
|
|
67
|
+
* back takes the whole of the light.
|
|
68
|
+
*/
|
|
69
|
+
pulseEffect(options, back, width, depth, theme) {
|
|
70
|
+
const ink = back.drawnColors(depth).color ?? ColorSpec.default();
|
|
71
|
+
const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
|
|
72
|
+
const loop = shimmer(PULSE_CURVE, width, PULSE_REACH, glow.getTruecolor(theme, true), 0);
|
|
73
|
+
return onColors(new Map([[ink.getTruecolor(theme, true).hex, 1]]), loop);
|
|
74
|
+
}
|
|
75
|
+
/** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
|
|
76
|
+
completeHalves(width) {
|
|
77
|
+
const completed = Math.min(this.total, Math.max(0, this.completed));
|
|
78
|
+
return this.total ? Math.trunc((width * 2 * completed) / this.total) : width * 2;
|
|
79
|
+
}
|
|
80
|
+
/** The bar `width` cells wide with `completeHalves` half cells filled, the rest in `back`. */
|
|
81
|
+
*track(options, width, completeHalves, back) {
|
|
82
|
+
const barCount = Math.floor(completeHalves / 2);
|
|
83
|
+
const halfBarCount = completeHalves % 2;
|
|
84
|
+
const isFinished = this.completed >= this.total;
|
|
37
85
|
const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
|
|
38
|
-
const back = getStyle(options, this.style);
|
|
39
86
|
const fillStyle = fill.isNull ? undefined : fill;
|
|
40
|
-
const
|
|
87
|
+
const backStyle = back.isNull ? undefined : back;
|
|
41
88
|
const bar = drawable(options, BAR, ASCII_BAR);
|
|
42
|
-
if (
|
|
43
|
-
yield new Segment(bar.repeat(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
if (barCount)
|
|
90
|
+
yield new Segment(bar.repeat(barCount), fillStyle);
|
|
91
|
+
if (halfBarCount)
|
|
92
|
+
yield new Segment(drawable(options, HALF_BAR_RIGHT, " "), fillStyle);
|
|
93
|
+
// The back is drawn in colour alone, so an output with none leaves it blank.
|
|
94
|
+
if (options.colorSystem === null)
|
|
95
|
+
return;
|
|
96
|
+
let remaining = width - barCount - halfBarCount;
|
|
97
|
+
if (remaining && !halfBarCount && barCount) {
|
|
98
|
+
yield new Segment(drawable(options, HALF_BAR_LEFT, " "), backStyle);
|
|
99
|
+
remaining -= 1;
|
|
47
100
|
}
|
|
101
|
+
if (remaining)
|
|
102
|
+
yield new Segment(bar.repeat(remaining), backStyle);
|
|
48
103
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { minimum: 4, maximum:
|
|
104
|
+
/** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
|
|
105
|
+
measure(options) {
|
|
106
|
+
return this.width === undefined ? { minimum: 4, maximum: options.maxWidth } : { minimum: this.width, maximum: this.width };
|
|
52
107
|
}
|
|
53
108
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt — interactive prompts for user input.
|
|
2
|
+
* Prompt — interactive prompts for user input, Rich's `rich/prompt.py`.
|
|
3
3
|
*
|
|
4
|
-
* [LAW:locality-or-seam] The renderable owns prompt logic (display,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `@promptctl/rich-js/node/prompt`; tests
|
|
9
|
-
*
|
|
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.
|
|
10
11
|
*
|
|
11
12
|
* [LAW:types-are-the-program] The `input: PromptInput` parameter is
|
|
12
13
|
* positional and required on every `*.ask()` static — not an optional in
|
|
@@ -20,20 +21,41 @@
|
|
|
20
21
|
* the caller at the node helper), not gatekeep-against-bugs — TS users
|
|
21
22
|
* never see it because the type already forbids the bad state.
|
|
22
23
|
*/
|
|
24
|
+
import { Console } from "../core/console.js";
|
|
25
|
+
import { RichText } from "../core/text.js";
|
|
23
26
|
/**
|
|
24
|
-
* Input capability: receives the
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
29
36
|
*/
|
|
30
|
-
export type PromptInput = (prompt:
|
|
37
|
+
export type PromptInput = (prompt: RichText, console: Console) => Promise<string>;
|
|
31
38
|
export interface PromptOptions<T> {
|
|
32
39
|
default?: T;
|
|
33
|
-
choices?: string[];
|
|
40
|
+
choices?: readonly string[];
|
|
34
41
|
caseSensitive?: boolean;
|
|
35
42
|
showChoices?: boolean;
|
|
36
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];
|
|
37
59
|
}
|
|
38
60
|
export declare class Prompt {
|
|
39
61
|
static ask(promptText: string, input: PromptInput, options?: PromptOptions<string>): Promise<string>;
|
|
@@ -45,5 +67,5 @@ export declare class FloatPrompt {
|
|
|
45
67
|
static ask(promptText: string, input: PromptInput, options?: PromptOptions<number>): Promise<number>;
|
|
46
68
|
}
|
|
47
69
|
export declare class Confirm {
|
|
48
|
-
static ask(promptText: string, input: PromptInput, options?:
|
|
70
|
+
static ask(promptText: string, input: PromptInput, options?: ConfirmOptions): Promise<boolean>;
|
|
49
71
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt — interactive prompts for user input.
|
|
2
|
+
* Prompt — interactive prompts for user input, Rich's `rich/prompt.py`.
|
|
3
3
|
*
|
|
4
|
-
* [LAW:locality-or-seam] The renderable owns prompt logic (display,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `@promptctl/rich-js/node/prompt`; tests
|
|
9
|
-
*
|
|
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.
|
|
10
11
|
*
|
|
11
12
|
* [LAW:types-are-the-program] The `input: PromptInput` parameter is
|
|
12
13
|
* positional and required on every `*.ask()` static — not an optional in
|
|
@@ -20,9 +21,35 @@
|
|
|
20
21
|
* the caller at the node helper), not gatekeep-against-bugs — TS users
|
|
21
22
|
* never see it because the type already forbids the bad state.
|
|
22
23
|
*/
|
|
24
|
+
import { Console } from "../core/console.js";
|
|
23
25
|
import { renderMarkup } from "../core/markup.js";
|
|
24
|
-
|
|
25
|
-
|
|
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) {
|
|
26
53
|
// [LAW:single-enforcer] Trust-boundary validation for non-TS callers
|
|
27
54
|
// (JS, or TS with `any` laundering). TS callers can't reach this branch
|
|
28
55
|
// because `PromptInput` is required at every static `.ask`. The message
|
|
@@ -33,93 +60,106 @@ function ask(promptText, input) {
|
|
|
33
60
|
"`@promptctl/rich-js/node/prompt` for Node, or supply a custom " +
|
|
34
61
|
"`PromptInput` for tests/browsers.");
|
|
35
62
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
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"));
|
|
38
128
|
}
|
|
39
129
|
// --- Prompt ---
|
|
40
130
|
export class Prompt {
|
|
41
|
-
static
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (showChoices && options?.choices) {
|
|
46
|
-
display += ` [${options.choices.join("/")}]`;
|
|
47
|
-
}
|
|
48
|
-
if (showDefault && options?.default !== undefined) {
|
|
49
|
-
display += ` (${options.default})`;
|
|
50
|
-
}
|
|
51
|
-
display += ":";
|
|
52
|
-
while (true) {
|
|
53
|
-
const answer = await ask(display, input);
|
|
54
|
-
const value = answer.trim();
|
|
55
|
-
if (value === "" && options?.default !== undefined) {
|
|
56
|
-
return options.default;
|
|
57
|
-
}
|
|
58
|
-
if (options?.choices) {
|
|
59
|
-
const caseSensitive = options.caseSensitive !== false;
|
|
60
|
-
const match = options.choices.find((c) => caseSensitive ? c === value : c.toLowerCase() === value.toLowerCase());
|
|
61
|
-
if (match)
|
|
62
|
-
return match;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
return value;
|
|
66
|
-
}
|
|
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);
|
|
67
135
|
}
|
|
68
136
|
}
|
|
69
137
|
export class IntPrompt {
|
|
70
|
-
static
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
if (showDefault && options?.default !== undefined) {
|
|
74
|
-
display += ` (${options.default})`;
|
|
75
|
-
}
|
|
76
|
-
display += ":";
|
|
77
|
-
while (true) {
|
|
78
|
-
const answer = await ask(display, input);
|
|
79
|
-
const value = answer.trim();
|
|
80
|
-
if (value === "" && options?.default !== undefined) {
|
|
81
|
-
return options.default;
|
|
82
|
-
}
|
|
83
|
-
const num = parseInt(value, 10);
|
|
84
|
-
if (!isNaN(num) && String(num) === value)
|
|
85
|
-
return num;
|
|
86
|
-
}
|
|
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);
|
|
87
141
|
}
|
|
88
142
|
}
|
|
89
143
|
export class FloatPrompt {
|
|
90
|
-
static
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
if (showDefault && options?.default !== undefined) {
|
|
94
|
-
display += ` (${options.default})`;
|
|
95
|
-
}
|
|
96
|
-
display += ":";
|
|
97
|
-
while (true) {
|
|
98
|
-
const answer = await ask(display, input);
|
|
99
|
-
const value = answer.trim();
|
|
100
|
-
if (value === "" && options?.default !== undefined) {
|
|
101
|
-
return options.default;
|
|
102
|
-
}
|
|
103
|
-
const num = parseFloat(value);
|
|
104
|
-
if (!isNaN(num))
|
|
105
|
-
return num;
|
|
106
|
-
}
|
|
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);
|
|
107
147
|
}
|
|
108
148
|
}
|
|
149
|
+
const CONFIRM_INVALID = new InvalidResponse("[prompt.invalid]Please enter Y or N");
|
|
109
150
|
export class Confirm {
|
|
110
|
-
static
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
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);
|
|
124
164
|
}
|
|
125
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 {};
|