@promptctl/rich-js 0.18.1 → 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.
- package/README.md +9 -7
- 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 +98 -18
- package/dist/core/color.js +331 -144
- 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/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 +150 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +105 -2
- package/dist/core/pretty.js +527 -97
- package/dist/core/protocol.d.ts +41 -3
- package/dist/core/protocol.js +20 -0
- 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/strip.d.ts +13 -2
- package/dist/core/strip.js +44 -23
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +97 -34
- 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 +31 -19
- package/dist/host/app.js +90 -60
- package/dist/host/index.d.ts +2 -1
- package/dist/host/terminal-host.d.ts +3 -2
- package/dist/index.d.ts +17 -10
- package/dist/index.js +11 -4
- 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 +3 -2
- 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 +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +87 -30
- package/dist/renderables/embed.js +134 -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/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 +104 -16
- package/dist/renderables/progress.js +205 -81
- package/dist/renderables/progressBar.d.ts +2 -2
- package/dist/renderables/progressBar.js +30 -19
- 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 +1 -0
- package/dist/renderables/rule.js +44 -48
- 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 +370 -192
- 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.js +2 -4
- 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 +14 -48
- 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/widget-app.d.ts +2 -0
- package/dist/widgets/widget-app.js +13 -6
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +11 -1
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress — displays continuously updated progress bars.
|
|
3
3
|
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
4
|
+
import type { Segment } from "../core/segment.js";
|
|
5
|
+
import { RichText } from "../core/text.js";
|
|
5
6
|
import { Console } from "../core/console.js";
|
|
7
|
+
import type { Style } from "../core/style.js";
|
|
8
|
+
import { ProgressBar } from "./progressBar.js";
|
|
9
|
+
import { type Clock } from "../core/clock.js";
|
|
10
|
+
import { type ColumnOptions } from "./table.js";
|
|
6
11
|
import type { Renderable, RenderOptions } from "../core/protocol.js";
|
|
7
12
|
export interface TaskOptions {
|
|
8
13
|
total?: number;
|
|
@@ -16,45 +21,122 @@ export interface TaskUpdateOptions {
|
|
|
16
21
|
visible?: boolean;
|
|
17
22
|
refresh?: boolean;
|
|
18
23
|
}
|
|
19
|
-
interface Task {
|
|
24
|
+
export interface Task {
|
|
20
25
|
id: number;
|
|
21
26
|
description: string;
|
|
22
27
|
total: number | undefined;
|
|
23
28
|
completed: number;
|
|
24
|
-
started: boolean;
|
|
25
29
|
visible: boolean;
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
/** When the task started, in seconds on its `Progress`'s clock; undefined until it has. */
|
|
31
|
+
startTime: number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Rich's `Task.elapsed`: seconds since the task started, as of the frame
|
|
34
|
+
* being drawn — `Progress` reads its clock once a frame and hands columns
|
|
35
|
+
* the result, so no column reads a clock. Undefined until the task starts.
|
|
36
|
+
*/
|
|
37
|
+
elapsed: number | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Rich's `Task.finished_time`: the elapsed time the first time an update
|
|
40
|
+
* finds a started task at its total, kept from then on, so a finished task
|
|
41
|
+
* stays finished however its count moves after.
|
|
42
|
+
*/
|
|
43
|
+
finishedTime: number | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Rich's `Task.speed`: steps a second over the updates that moved the task
|
|
46
|
+
* in the `SPEED_ESTIMATE_PERIOD` before its latest update — measured back from
|
|
47
|
+
* that update, not from now, so a task that stops being updated keeps the
|
|
48
|
+
* speed it had. Undefined until the task has started and two such updates lie
|
|
49
|
+
* apart in time.
|
|
50
|
+
*/
|
|
51
|
+
speed: number | undefined;
|
|
28
52
|
}
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
/**
|
|
54
|
+
* One cell of a `Progress` row, made for the task on that row. Not a
|
|
55
|
+
* `Renderable`: a column has nothing to draw without a task, as the
|
|
56
|
+
* reference's `ProgressColumn` is a callable of one. What it returns is the
|
|
57
|
+
* cell's content, which the progress grid lays out like any table cell's —
|
|
58
|
+
* width, wrapping and justify are decided once, in the cell, as Rich's
|
|
59
|
+
* `make_tasks_table` decides them. [LAW:single-enforcer]
|
|
60
|
+
*/
|
|
61
|
+
export interface ProgressColumn {
|
|
62
|
+
/**
|
|
63
|
+
* The grid column this one is laid out in, Rich's `get_table_column()`:
|
|
64
|
+
* whether its cell may wrap, how it justifies, its share of the width.
|
|
65
|
+
*/
|
|
66
|
+
readonly tableColumn: ColumnOptions;
|
|
67
|
+
render(task: Task): Renderable;
|
|
31
68
|
}
|
|
32
69
|
export declare class TextColumn implements ProgressColumn {
|
|
33
70
|
readonly format: string;
|
|
71
|
+
/** Text is cut, never wrapped, as Rich's `TextColumn` defaults to `Column(no_wrap=True)`. */
|
|
72
|
+
readonly tableColumn: ColumnOptions;
|
|
34
73
|
constructor(format?: string);
|
|
35
|
-
render(
|
|
74
|
+
render(task: Task): RichText;
|
|
36
75
|
}
|
|
37
76
|
export declare class BarColumn implements ProgressColumn {
|
|
38
77
|
readonly barWidth: number;
|
|
78
|
+
readonly tableColumn: ColumnOptions;
|
|
39
79
|
constructor(barWidth?: number);
|
|
40
|
-
render(
|
|
80
|
+
render(task: Task): ProgressBar;
|
|
41
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Rich's `{task.percentage:>3.0f}%`: the percentage right-aligned in three
|
|
84
|
+
* cells, so the column is four wide from 0% to 100% and the row does not
|
|
85
|
+
* shift as it counts. A task with no total shows nothing, Rich's
|
|
86
|
+
* `text_format_no_percentage`.
|
|
87
|
+
*/
|
|
42
88
|
export declare class TaskProgressColumn implements ProgressColumn {
|
|
43
|
-
|
|
89
|
+
/** A `TextColumn` in Rich, and laid out as one. */
|
|
90
|
+
readonly tableColumn: ColumnOptions;
|
|
91
|
+
render(task: Task): RichText;
|
|
92
|
+
}
|
|
93
|
+
export interface TimeRemainingColumnOptions {
|
|
94
|
+
/**
|
|
95
|
+
* Once the task is finished, draw the time it took, styled
|
|
96
|
+
* `progress.elapsed`, in place of the zero left to go: Rich's
|
|
97
|
+
* `elapsed_when_finished`. Defaults to false.
|
|
98
|
+
*/
|
|
99
|
+
elapsedWhenFinished?: boolean;
|
|
44
100
|
}
|
|
45
101
|
export declare class TimeRemainingColumn implements ProgressColumn {
|
|
46
|
-
|
|
102
|
+
readonly tableColumn: ColumnOptions;
|
|
103
|
+
readonly elapsedWhenFinished: boolean;
|
|
104
|
+
constructor(options?: TimeRemainingColumnOptions);
|
|
105
|
+
render(task: Task): RichText;
|
|
47
106
|
}
|
|
48
107
|
export declare class TimeElapsedColumn implements ProgressColumn {
|
|
49
|
-
|
|
108
|
+
readonly tableColumn: ColumnOptions;
|
|
109
|
+
render(task: Task): RichText;
|
|
110
|
+
}
|
|
111
|
+
export interface SpinnerColumnOptions {
|
|
112
|
+
/** The frame's style. Defaults to `progress.spinner`, a name its console's theme resolves. */
|
|
113
|
+
style?: string | Style;
|
|
114
|
+
/** Multiplies the spinner's frame rate. Defaults to 1. */
|
|
115
|
+
speed?: number;
|
|
116
|
+
/** Drawn in place of the frame once the task reaches its total; a string is markup. Defaults to `" "`. */
|
|
117
|
+
finishedText?: string | RichText;
|
|
50
118
|
}
|
|
51
119
|
export declare class SpinnerColumn implements ProgressColumn {
|
|
120
|
+
readonly tableColumn: ColumnOptions;
|
|
52
121
|
private _spinner;
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
private _finishedText;
|
|
123
|
+
constructor(spinnerName?: string, options?: SpinnerColumnOptions);
|
|
124
|
+
render(task: Task): Renderable;
|
|
125
|
+
}
|
|
126
|
+
export interface MofNCompleteColumnOptions {
|
|
127
|
+
/** Drawn between the two counts: Rich's `separator`. Defaults to `"/"`. */
|
|
128
|
+
separator?: string;
|
|
55
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Rich's `MofNCompleteColumn`: both counts as whole numbers, the completed one
|
|
132
|
+
* padded to the total's width so the column holds still as it counts up to its
|
|
133
|
+
* total, styled `progress.download`.
|
|
134
|
+
*/
|
|
56
135
|
export declare class MofNCompleteColumn implements ProgressColumn {
|
|
57
|
-
|
|
136
|
+
readonly tableColumn: ColumnOptions;
|
|
137
|
+
readonly separator: string;
|
|
138
|
+
constructor(options?: MofNCompleteColumnOptions);
|
|
139
|
+
render(task: Task): RichText;
|
|
58
140
|
}
|
|
59
141
|
export interface ProgressOptions {
|
|
60
142
|
refreshPerSecond?: number;
|
|
@@ -62,6 +144,11 @@ export interface ProgressOptions {
|
|
|
62
144
|
transient?: boolean;
|
|
63
145
|
expand?: boolean;
|
|
64
146
|
console?: Console;
|
|
147
|
+
/**
|
|
148
|
+
* What tasks are timed by and frames are drawn on, Rich's `get_time`;
|
|
149
|
+
* defaults to the platform's monotonic clock. Handed on to the `Live`.
|
|
150
|
+
*/
|
|
151
|
+
clock?: Clock;
|
|
65
152
|
}
|
|
66
153
|
export declare class Progress implements Renderable {
|
|
67
154
|
private _columns;
|
|
@@ -69,9 +156,11 @@ export declare class Progress implements Renderable {
|
|
|
69
156
|
private _nextId;
|
|
70
157
|
private _live;
|
|
71
158
|
private _console;
|
|
159
|
+
private readonly _clock;
|
|
72
160
|
readonly expand: boolean;
|
|
73
161
|
constructor(...columns: (ProgressColumn | ProgressOptions)[]);
|
|
74
162
|
get console(): Console;
|
|
163
|
+
/** Rich's `Progress.finished`: every task finished, and so true with none. */
|
|
75
164
|
get finished(): boolean;
|
|
76
165
|
static getDefaultColumns(): ProgressColumn[];
|
|
77
166
|
addTask(description: string, options?: TaskOptions): number;
|
|
@@ -87,4 +176,3 @@ export declare function track<T>(iterable: Iterable<T>, options?: {
|
|
|
87
176
|
total?: number;
|
|
88
177
|
console?: Console;
|
|
89
178
|
}): Iterable<T>;
|
|
90
|
-
export {};
|
|
@@ -1,100 +1,215 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Progress — displays continuously updated progress bars.
|
|
3
3
|
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
5
4
|
import { RichText } from "../core/text.js";
|
|
6
5
|
import { Console } from "../core/console.js";
|
|
7
|
-
import { escape as escapeMarkup } from "../core/markup.js";
|
|
8
|
-
import { embeddedText } from "./embed.js";
|
|
6
|
+
import { escape as escapeMarkup, readStr, renderStr } from "../core/markup.js";
|
|
9
7
|
import { ProgressBar } from "./progressBar.js";
|
|
10
8
|
import { Spinner } from "./spinner.js";
|
|
11
9
|
import { Live } from "./live.js";
|
|
10
|
+
import { systemClock } from "../core/clock.js";
|
|
12
11
|
import { Table } from "./table.js";
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Rich's defaults for `speed_estimate_period` and its sample deque's
|
|
14
|
+
* `maxlen`: an update forgets samples older than the period, and the newest
|
|
15
|
+
* thousand are all a task keeps.
|
|
16
|
+
*/
|
|
17
|
+
const SPEED_ESTIMATE_PERIOD = 30;
|
|
18
|
+
const MAX_SAMPLES = 1000;
|
|
19
|
+
/**
|
|
20
|
+
* Rich's `Progress.update` bookkeeping on its sample deque, in place: drop the
|
|
21
|
+
* samples older than the period as of `now`, then add this update's if it
|
|
22
|
+
* moved the task forward, keeping the newest `MAX_SAMPLES`. The samples are in
|
|
23
|
+
* time order, so the stale ones are a prefix.
|
|
24
|
+
*/
|
|
25
|
+
function recordSample(samples, now, moved) {
|
|
26
|
+
const firstFresh = samples.findIndex((sample) => sample.timestamp >= now - SPEED_ESTIMATE_PERIOD);
|
|
27
|
+
samples.splice(0, firstFresh === -1 ? samples.length : firstFresh);
|
|
28
|
+
if (moved > 0)
|
|
29
|
+
samples.push({ timestamp: now, completed: moved });
|
|
30
|
+
samples.splice(0, samples.length - MAX_SAMPLES);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Rich's `Task.speed` over a started task's samples: everything moved after
|
|
34
|
+
* the first sample, over the time from the first to the last. No samples, or
|
|
35
|
+
* none apart in time, is no speed.
|
|
36
|
+
*/
|
|
37
|
+
function sampleSpeed(samples) {
|
|
38
|
+
const span = (samples.at(-1)?.timestamp ?? 0) - (samples[0]?.timestamp ?? 0);
|
|
39
|
+
if (span === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return samples.slice(1).reduce((sum, sample) => sum + sample.completed, 0) / span;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A count as a task takes it in. [LAW:single-enforcer] `addTask` and
|
|
45
|
+
* `updateTask` are where counts enter, so a NaN or an Infinity is refused there,
|
|
46
|
+
* with the caller's stack, rather than drawn as `NaN%` by one column and thrown
|
|
47
|
+
* on a refresh tick by another.
|
|
48
|
+
*/
|
|
49
|
+
function finiteCount(field, value) {
|
|
50
|
+
if (!Number.isFinite(value))
|
|
51
|
+
throw new RangeError(`progress task ${field} must be a finite number, got ${value}`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
/** Rich's `Task.finished`. [LAW:one-source-of-truth] The one predicate every reader of "finished" asks. */
|
|
55
|
+
function finished(task) {
|
|
56
|
+
return task.finishedTime !== undefined;
|
|
57
|
+
}
|
|
14
58
|
export class TextColumn {
|
|
15
59
|
format;
|
|
60
|
+
/** Text is cut, never wrapped, as Rich's `TextColumn` defaults to `Column(no_wrap=True)`. */
|
|
61
|
+
tableColumn = { noWrap: true };
|
|
16
62
|
constructor(format) {
|
|
17
63
|
this.format = format ?? "{task.description}";
|
|
18
64
|
}
|
|
19
|
-
|
|
65
|
+
render(task) {
|
|
20
66
|
// [LAW:single-enforcer] Format strings flow through the markup parser so
|
|
21
67
|
// tags like `[progress.description]` become Style spans, not literal text.
|
|
22
68
|
// Task descriptions are escaped first to prevent injection of stray tags.
|
|
23
|
-
const description = escapeMarkup(task
|
|
69
|
+
const description = escapeMarkup(task.description);
|
|
24
70
|
// Use a callback so `$&`/`$1`/`$$` in the task description aren't
|
|
25
71
|
// reinterpreted by String.replace as replacement patterns.
|
|
26
72
|
const formatted = this.format.replace(/\{task\.description\}/g, () => description);
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
73
|
+
// Markup whatever the console says, and never highlighted: Rich's
|
|
74
|
+
// `TextColumn` reads its format as markup on its own `markup=True` and
|
|
75
|
+
// highlights only with a highlighter it was given, not the console's.
|
|
76
|
+
return renderStr(formatted, { markup: true });
|
|
30
77
|
}
|
|
31
78
|
}
|
|
32
79
|
export class BarColumn {
|
|
33
80
|
barWidth;
|
|
81
|
+
tableColumn = {};
|
|
34
82
|
constructor(barWidth) {
|
|
35
83
|
this.barWidth = barWidth ?? 40;
|
|
36
84
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
total: task
|
|
40
|
-
completed: task
|
|
85
|
+
render(task) {
|
|
86
|
+
return new ProgressBar({
|
|
87
|
+
total: task.total ?? 100,
|
|
88
|
+
completed: task.completed,
|
|
41
89
|
width: this.barWidth,
|
|
42
90
|
});
|
|
43
|
-
yield* bar.render(_options);
|
|
44
91
|
}
|
|
45
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Rich's `{task.percentage:>3.0f}%`: the percentage right-aligned in three
|
|
95
|
+
* cells, so the column is four wide from 0% to 100% and the row does not
|
|
96
|
+
* shift as it counts. A task with no total shows nothing, Rich's
|
|
97
|
+
* `text_format_no_percentage`.
|
|
98
|
+
*/
|
|
46
99
|
export class TaskProgressColumn {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
/** A `TextColumn` in Rich, and laid out as one. */
|
|
101
|
+
tableColumn = { noWrap: true };
|
|
102
|
+
render(task) {
|
|
103
|
+
const text = task.total === undefined ? "" : `${String(roundHalfEven(percentage(task))).padStart(3)}%`;
|
|
104
|
+
return new RichText(text, { style: "progress.percentage" });
|
|
52
105
|
}
|
|
53
106
|
}
|
|
107
|
+
/** Rich's `Task.percentage`: 0 without a total, and clamped to [0, 100]. */
|
|
108
|
+
function percentage(task) {
|
|
109
|
+
return task.total ? Math.min(100, Math.max(0, (task.completed / task.total) * 100)) : 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Python's `format(x, ".0f")`: the nearest integer to the double's exact
|
|
113
|
+
* value, a tie going to the even one. `Math.round` is exact too but sends a
|
|
114
|
+
* tie up, so only a tie is corrected.
|
|
115
|
+
*/
|
|
116
|
+
function roundHalfEven(x) {
|
|
117
|
+
const up = Math.round(x);
|
|
118
|
+
return up - x === 0.5 && up % 2 !== 0 ? up - 1 : up;
|
|
119
|
+
}
|
|
54
120
|
export class TimeRemainingColumn {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
121
|
+
tableColumn = {};
|
|
122
|
+
elapsedWhenFinished;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.elapsedWhenFinished = options?.elapsedWhenFinished ?? false;
|
|
125
|
+
}
|
|
126
|
+
render(task) {
|
|
127
|
+
const took = this.elapsedWhenFinished && finished(task) ? task.finishedTime : undefined;
|
|
128
|
+
const time = took ?? timeRemaining(task);
|
|
129
|
+
const style = took === undefined ? "progress.remaining" : "progress.elapsed";
|
|
130
|
+
// A task with no total has no time to estimate, and Rich draws its cell empty.
|
|
131
|
+
return new RichText(task.total === undefined ? "" : clockFace(time), { style });
|
|
65
132
|
}
|
|
66
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Rich's `Task.time_remaining`: nothing left once the task is finished,
|
|
136
|
+
* however long ago that was, unknown until it has a speed against a total,
|
|
137
|
+
* and otherwise whole seconds, rounded up.
|
|
138
|
+
*/
|
|
139
|
+
function timeRemaining(task) {
|
|
140
|
+
if (finished(task))
|
|
141
|
+
return 0;
|
|
142
|
+
if (!task.speed || task.total === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
return Math.ceil((task.total - task.completed) / task.speed);
|
|
145
|
+
}
|
|
67
146
|
export class TimeElapsedColumn {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
const elapsed = (Date.now() - task.startTime) / 1000;
|
|
75
|
-
yield new Segment(formatTime(elapsed), style);
|
|
147
|
+
tableColumn = {};
|
|
148
|
+
render(task) {
|
|
149
|
+
// Rich's `finished_time if finished else elapsed`: a finished task's clock
|
|
150
|
+
// holds where it stopped, and one never started has no time to show.
|
|
151
|
+
const time = finished(task) ? task.finishedTime : task.elapsed;
|
|
152
|
+
return new RichText(clockFace(time), { style: "progress.elapsed" });
|
|
76
153
|
}
|
|
77
154
|
}
|
|
155
|
+
/** A time as the time columns draw it, `-:--:--` when there is none to draw. */
|
|
156
|
+
function clockFace(seconds) {
|
|
157
|
+
return seconds === undefined ? "-:--:--" : formatTime(seconds);
|
|
158
|
+
}
|
|
78
159
|
export class SpinnerColumn {
|
|
160
|
+
tableColumn = {};
|
|
79
161
|
_spinner;
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
_finishedText;
|
|
163
|
+
constructor(spinnerName, options) {
|
|
164
|
+
this._spinner = new Spinner(spinnerName, "", {
|
|
165
|
+
style: options?.style ?? "progress.spinner",
|
|
166
|
+
speed: options?.speed,
|
|
167
|
+
});
|
|
168
|
+
// Markup whatever the console says, as Rich's `Text.from_markup`. A
|
|
169
|
+
// `RichText` is copied, since the caller still holds it.
|
|
170
|
+
const finishedText = options?.finishedText ?? " ";
|
|
171
|
+
this._finishedText = typeof finishedText === "string" ? readStr(finishedText, true) : finishedText.copy();
|
|
82
172
|
}
|
|
83
|
-
|
|
84
|
-
|
|
173
|
+
render(task) {
|
|
174
|
+
return finished(task) ? this._finishedText : this._spinner;
|
|
85
175
|
}
|
|
86
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Rich's `MofNCompleteColumn`: both counts as whole numbers, the completed one
|
|
179
|
+
* padded to the total's width so the column holds still as it counts up to its
|
|
180
|
+
* total, styled `progress.download`.
|
|
181
|
+
*/
|
|
87
182
|
export class MofNCompleteColumn {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
183
|
+
tableColumn = {};
|
|
184
|
+
separator;
|
|
185
|
+
constructor(options) {
|
|
186
|
+
this.separator = options?.separator ?? "/";
|
|
187
|
+
}
|
|
188
|
+
render(task) {
|
|
189
|
+
const total = task.total === undefined ? "?" : wholeNumber(task.total);
|
|
190
|
+
const completed = wholeNumber(task.completed).padStart(total.length);
|
|
191
|
+
return new RichText(`${completed}${this.separator}${total}`, { style: "progress.download" });
|
|
92
192
|
}
|
|
93
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Python's `str(int(x))`: truncated toward zero and written out in full,
|
|
196
|
+
* never in exponent form. `Progress` admits only finite counts, so `BigInt`
|
|
197
|
+
* has nothing here to refuse.
|
|
198
|
+
*/
|
|
199
|
+
function wholeNumber(x) {
|
|
200
|
+
return BigInt(Math.trunc(x)).toString();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Rich's clock face: `int()` the seconds, then Python's floor `divmod` by 60
|
|
204
|
+
* twice, so a negative estimate — a task past its total — reads as Rich's
|
|
205
|
+
* does, `-3` as `-1:59:57`.
|
|
206
|
+
*/
|
|
94
207
|
function formatTime(seconds) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const
|
|
208
|
+
const whole = Math.trunc(seconds);
|
|
209
|
+
const minutes = Math.floor(whole / 60);
|
|
210
|
+
const h = Math.floor(minutes / 60);
|
|
211
|
+
const m = minutes - h * 60;
|
|
212
|
+
const s = whole - minutes * 60;
|
|
98
213
|
return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
99
214
|
}
|
|
100
215
|
export class Progress {
|
|
@@ -103,6 +218,7 @@ export class Progress {
|
|
|
103
218
|
_nextId;
|
|
104
219
|
_live;
|
|
105
220
|
_console;
|
|
221
|
+
_clock;
|
|
106
222
|
expand;
|
|
107
223
|
constructor(...columns) {
|
|
108
224
|
// Last arg might be options
|
|
@@ -122,10 +238,12 @@ export class Progress {
|
|
|
122
238
|
this._columns = cols;
|
|
123
239
|
this._tasks = new Map();
|
|
124
240
|
this._nextId = 1;
|
|
125
|
-
this._console = opts.console ?? new Console(
|
|
241
|
+
this._console = opts.console ?? new Console();
|
|
126
242
|
this.expand = opts.expand ?? false;
|
|
243
|
+
this._clock = opts.clock ?? systemClock();
|
|
127
244
|
this._live = new Live(this, {
|
|
128
245
|
console: this._console,
|
|
246
|
+
clock: this._clock,
|
|
129
247
|
refreshPerSecond: opts.refreshPerSecond ?? 10,
|
|
130
248
|
autoRefresh: opts.autoRefresh,
|
|
131
249
|
transient: opts.transient,
|
|
@@ -134,13 +252,9 @@ export class Progress {
|
|
|
134
252
|
get console() {
|
|
135
253
|
return this._console;
|
|
136
254
|
}
|
|
255
|
+
/** Rich's `Progress.finished`: every task finished, and so true with none. */
|
|
137
256
|
get finished() {
|
|
138
|
-
|
|
139
|
-
if (!task.started || (task.total !== undefined && task.completed < task.total)) {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return this._tasks.size > 0;
|
|
257
|
+
return [...this._tasks.values()].every(finished);
|
|
144
258
|
}
|
|
145
259
|
static getDefaultColumns() {
|
|
146
260
|
return [
|
|
@@ -155,12 +269,12 @@ export class Progress {
|
|
|
155
269
|
const task = {
|
|
156
270
|
id,
|
|
157
271
|
description,
|
|
158
|
-
total: options?.total,
|
|
272
|
+
total: options?.total === undefined ? undefined : finiteCount("total", options.total),
|
|
159
273
|
completed: 0,
|
|
160
|
-
started: options?.start !== false,
|
|
161
274
|
visible: options?.visible !== false,
|
|
162
|
-
startTime:
|
|
163
|
-
|
|
275
|
+
startTime: options?.start === false ? undefined : this._clock.now(),
|
|
276
|
+
finishedTime: undefined,
|
|
277
|
+
samples: [],
|
|
164
278
|
};
|
|
165
279
|
this._tasks.set(id, task);
|
|
166
280
|
return id;
|
|
@@ -169,24 +283,32 @@ export class Progress {
|
|
|
169
283
|
const task = this._tasks.get(taskId);
|
|
170
284
|
if (!task)
|
|
171
285
|
return;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
286
|
+
// Rich's order, `completed` then `advance`, checked whole before any field
|
|
287
|
+
// is written, so a refused update leaves the task as it was.
|
|
288
|
+
const advance = finiteCount("advance", options.advance ?? 0);
|
|
289
|
+
const completed = finiteCount("completed", (options.completed ?? task.completed) + advance);
|
|
290
|
+
const now = this._clock.now();
|
|
291
|
+
recordSample(task.samples, now, completed - task.completed);
|
|
292
|
+
task.completed = completed;
|
|
176
293
|
if (options.description !== undefined)
|
|
177
294
|
task.description = options.description;
|
|
178
295
|
if (options.visible !== undefined)
|
|
179
296
|
task.visible = options.visible;
|
|
297
|
+
if (!finished(task) &&
|
|
298
|
+
task.startTime !== undefined &&
|
|
299
|
+
task.total !== undefined &&
|
|
300
|
+
task.completed >= task.total) {
|
|
301
|
+
task.finishedTime = now - task.startTime;
|
|
302
|
+
}
|
|
180
303
|
if (options.refresh) {
|
|
181
304
|
this._live.update(this, { refresh: true });
|
|
182
305
|
}
|
|
183
306
|
}
|
|
184
307
|
startTask(taskId) {
|
|
185
308
|
const task = this._tasks.get(taskId);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
task.startTime
|
|
189
|
-
}
|
|
309
|
+
// Rich's `start_task`: a task already running keeps the time it started.
|
|
310
|
+
if (task)
|
|
311
|
+
task.startTime ??= this._clock.now();
|
|
190
312
|
}
|
|
191
313
|
start() {
|
|
192
314
|
this._live.start();
|
|
@@ -198,23 +320,25 @@ export class Progress {
|
|
|
198
320
|
this._live.refresh();
|
|
199
321
|
}
|
|
200
322
|
*render(options) {
|
|
323
|
+
// Rich's `make_tasks_table`: each column laid out as it says.
|
|
201
324
|
const table = Table.grid({ expand: this.expand });
|
|
202
|
-
for (const
|
|
203
|
-
table.addColumn();
|
|
325
|
+
for (const col of this._columns) {
|
|
326
|
+
table.addColumn(undefined, col.tableColumn);
|
|
204
327
|
}
|
|
205
|
-
|
|
206
|
-
|
|
328
|
+
// [LAW:effects-at-boundaries] One read of the clock a frame, handed to
|
|
329
|
+
// every column as the task's `elapsed`.
|
|
330
|
+
const now = this._clock.now();
|
|
331
|
+
for (const { samples, ...state } of this._tasks.values()) {
|
|
332
|
+
if (!state.visible)
|
|
207
333
|
continue;
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
table.addRow(...cells);
|
|
334
|
+
const { startTime } = state;
|
|
335
|
+
const started = startTime !== undefined;
|
|
336
|
+
const task = {
|
|
337
|
+
...state,
|
|
338
|
+
elapsed: started ? now - startTime : undefined,
|
|
339
|
+
speed: started ? sampleSpeed(samples) : undefined,
|
|
340
|
+
};
|
|
341
|
+
table.addRow(...this._columns.map((col) => col.render(task)));
|
|
218
342
|
}
|
|
219
343
|
yield* table.render(options);
|
|
220
344
|
}
|
|
@@ -22,9 +22,9 @@ export declare class ProgressBar implements Renderable, Measurable {
|
|
|
22
22
|
readonly completeStyle: string | Style;
|
|
23
23
|
readonly finishedStyle: string | Style;
|
|
24
24
|
constructor(options?: ProgressBarOptions);
|
|
25
|
-
get percentComplete(): number;
|
|
26
25
|
render(options: RenderOptions): Iterable<Segment>;
|
|
27
|
-
|
|
26
|
+
/** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
|
|
27
|
+
measure(options: RenderOptions): {
|
|
28
28
|
minimum: number;
|
|
29
29
|
maximum: number;
|
|
30
30
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ProgressBar — a visual progress bar rendered with block characters.
|
|
3
3
|
*/
|
|
4
|
+
import { cellCount } from "../core/cells.js";
|
|
4
5
|
import { Segment } from "../core/segment.js";
|
|
5
6
|
import { NULL_STYLE } from "../core/style.js";
|
|
6
7
|
import { drawable, getStyle } from "../core/protocol.js";
|
|
7
8
|
const BAR = "━";
|
|
8
9
|
const ASCII_BAR = "-";
|
|
10
|
+
const HALF_BAR_RIGHT = "╸";
|
|
11
|
+
const HALF_BAR_LEFT = "╺";
|
|
9
12
|
export class ProgressBar {
|
|
10
13
|
total;
|
|
11
14
|
completed;
|
|
@@ -23,31 +26,39 @@ export class ProgressBar {
|
|
|
23
26
|
this.completeStyle = options?.completeStyle ?? "bar.complete";
|
|
24
27
|
this.finishedStyle = options?.finishedStyle ?? "bar.finished";
|
|
25
28
|
}
|
|
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
29
|
*render(options) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
// Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
|
|
31
|
+
// [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
|
|
32
|
+
const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
|
|
33
|
+
// Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and
|
|
34
|
+
// a total of zero is a bar already full.
|
|
35
|
+
const completed = Math.min(this.total, Math.max(0, this.completed));
|
|
36
|
+
const completeHalves = this.total ? Math.trunc((width * 2 * completed) / this.total) : width * 2;
|
|
37
|
+
const barCount = Math.floor(completeHalves / 2);
|
|
38
|
+
const halfBarCount = completeHalves % 2;
|
|
39
|
+
const isFinished = this.completed >= this.total;
|
|
37
40
|
const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
|
|
38
41
|
const back = getStyle(options, this.style);
|
|
39
42
|
const fillStyle = fill.isNull ? undefined : fill;
|
|
40
|
-
const
|
|
43
|
+
const backStyle = back.isNull ? undefined : back;
|
|
41
44
|
const bar = drawable(options, BAR, ASCII_BAR);
|
|
42
|
-
if (
|
|
43
|
-
yield new Segment(bar.repeat(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (barCount)
|
|
46
|
+
yield new Segment(bar.repeat(barCount), fillStyle);
|
|
47
|
+
if (halfBarCount)
|
|
48
|
+
yield new Segment(drawable(options, HALF_BAR_RIGHT, " "), fillStyle);
|
|
49
|
+
// The back is drawn in colour alone, so an output with none leaves it blank.
|
|
50
|
+
if (options.colorSystem === null)
|
|
51
|
+
return;
|
|
52
|
+
let remaining = width - barCount - halfBarCount;
|
|
53
|
+
if (remaining && !halfBarCount && barCount) {
|
|
54
|
+
yield new Segment(drawable(options, HALF_BAR_LEFT, " "), backStyle);
|
|
55
|
+
remaining -= 1;
|
|
47
56
|
}
|
|
57
|
+
if (remaining)
|
|
58
|
+
yield new Segment(bar.repeat(remaining), backStyle);
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { minimum: 4, maximum:
|
|
60
|
+
/** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
|
|
61
|
+
measure(options) {
|
|
62
|
+
return this.width === undefined ? { minimum: 4, maximum: options.maxWidth } : { minimum: this.width, maximum: this.width };
|
|
52
63
|
}
|
|
53
64
|
}
|