@promptctl/rich-js 0.24.1 → 0.25.1
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 +7 -0
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +175 -10
- package/dist/core/animation.js +283 -15
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cells.d.ts +23 -13
- package/dist/core/cells.js +58 -48
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +24 -33
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +44 -39
- package/dist/core/easing.d.ts +30 -0
- package/dist/core/easing.js +65 -20
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +2 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +2 -2
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/pretty.js +9 -10
- package/dist/core/protocol.d.ts +100 -39
- package/dist/core/protocol.js +167 -21
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +25 -5
- package/dist/core/segment.d.ts +13 -5
- package/dist/core/segment.js +23 -9
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +98 -29
- package/dist/core/strip.js +211 -150
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +57 -28
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +116 -124
- package/dist/core/timeline.d.ts +30 -5
- package/dist/core/timeline.js +43 -7
- package/dist/core/tween.d.ts +1 -1
- package/dist/core/tween.js +5 -3
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +71 -37
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +24 -12
- package/dist/index.js +9 -4
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/align.d.ts +2 -3
- package/dist/renderables/align.js +0 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +3 -4
- package/dist/renderables/columns.js +2 -7
- package/dist/renderables/constrain.d.ts +2 -3
- package/dist/renderables/constrain.js +0 -3
- package/dist/renderables/effect.d.ts +158 -63
- package/dist/renderables/effect.js +378 -155
- package/dist/renderables/effects.d.ts +122 -11
- package/dist/renderables/effects.js +238 -94
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +16 -8
- package/dist/renderables/flexStrip.d.ts +2 -3
- package/dist/renderables/flexStrip.js +0 -4
- package/dist/renderables/frames.d.ts +35 -54
- package/dist/renderables/frames.js +43 -58
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +3 -4
- package/dist/renderables/group.js +0 -3
- package/dist/renderables/layout.d.ts +12 -12
- package/dist/renderables/layout.js +40 -46
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +4 -5
- package/dist/renderables/list-view.js +9 -13
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +88 -41
- package/dist/renderables/markdown-parse.d.ts +4 -4
- package/dist/renderables/markdown-parse.js +52 -46
- package/dist/renderables/markdown-url.js +11 -11
- package/dist/renderables/padding.d.ts +4 -5
- package/dist/renderables/padding.js +0 -3
- package/dist/renderables/panel.d.ts +14 -16
- package/dist/renderables/panel.js +6 -14
- package/dist/renderables/progress.d.ts +83 -34
- package/dist/renderables/progress.js +182 -95
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +32 -9
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.js +2 -1
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +35 -14
- package/dist/renderables/spinner.js +65 -25
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +3 -2
- package/dist/renderables/table.d.ts +34 -40
- package/dist/renderables/table.js +41 -63
- package/dist/renderables/traceback.d.ts +2 -2
- package/dist/renderables/traceback.js +6 -5
- package/dist/renderables/tree.d.ts +27 -25
- package/dist/renderables/tree.js +104 -58
- package/dist/renderables/viewport.d.ts +75 -44
- package/dist/renderables/viewport.js +145 -82
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +5 -5
- package/dist/themes/ramp.d.ts +46 -3
- package/dist/themes/ramp.js +61 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +2 -0
- package/dist/widgets/focus-manager.js +6 -4
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +2 -3
- package/dist/widgets/static-item.js +0 -3
- package/dist/widgets/text-input.d.ts +28 -32
- package/dist/widgets/text-input.js +360 -346
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +14 -4
- package/dist/widgets/widget-app.js +36 -7
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -18
- package/package.json +8 -1
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Progress — displays continuously updated progress bars.
|
|
3
3
|
*/
|
|
4
4
|
import { RichText } from "../core/text.js";
|
|
5
|
-
import { Console } from "../core/console.js";
|
|
6
5
|
import { escape as escapeMarkup, readStr, renderStr } from "../core/markup.js";
|
|
7
6
|
import { ProgressBar } from "./progressBar.js";
|
|
8
7
|
import { Spinner } from "./spinner.js";
|
|
9
8
|
import { Live } from "./live.js";
|
|
10
|
-
import { systemClock } from "../core/clock.js";
|
|
11
9
|
import { Table } from "./table.js";
|
|
12
10
|
import { render } from "../core/protocol.js";
|
|
11
|
+
import { Animation } from "../core/animation.js";
|
|
12
|
+
import { Timeline } from "../core/timeline.js";
|
|
13
|
+
import { tween } from "../core/tween.js";
|
|
14
|
+
import { ListState, State } from "../core/state.js";
|
|
13
15
|
/**
|
|
14
16
|
* Rich's defaults for `speed_estimate_period` and its sample deque's
|
|
15
17
|
* `maxlen`: an update forgets samples older than the period, and the newest
|
|
@@ -18,28 +20,32 @@ import { render } from "../core/protocol.js";
|
|
|
18
20
|
const SPEED_ESTIMATE_PERIOD = 30;
|
|
19
21
|
const MAX_SAMPLES = 1000;
|
|
20
22
|
/**
|
|
21
|
-
* Rich's `Progress.update` bookkeeping on its sample deque,
|
|
22
|
-
* samples older than the period as of `now`, then add this
|
|
23
|
-
* moved the task forward, keeping the newest `MAX_SAMPLES`. The samples are in
|
|
24
|
-
*
|
|
23
|
+
* Rich's `Progress.update` bookkeeping on its sample deque, as the samples it
|
|
24
|
+
* leaves: drop the samples older than the period as of `now`, then add this
|
|
25
|
+
* update's if it moved the task forward, keeping the newest `MAX_SAMPLES`. The samples are in
|
|
26
|
+
* the order the updates came, which is time order unless the host's timeline
|
|
27
|
+
* ran back between them, so the stale ones are found by time, not position.
|
|
25
28
|
*/
|
|
26
29
|
function recordSample(samples, now, moved) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (moved > 0)
|
|
30
|
-
samples.push({ timestamp: now, completed: moved });
|
|
31
|
-
samples.splice(0, samples.length - MAX_SAMPLES);
|
|
30
|
+
const fresh = samples.filter((sample) => sample.timestamp >= now - SPEED_ESTIMATE_PERIOD);
|
|
31
|
+
return [...fresh, ...(moved > 0 ? [{ timestamp: now, completed: moved }] : [])].slice(-MAX_SAMPLES);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* Rich's `Task.speed` over a started task's samples: everything moved after
|
|
35
|
-
* the
|
|
36
|
-
*
|
|
35
|
+
* the earliest sample, over the time from the earliest to the latest — Rich's
|
|
36
|
+
* first and last, when time only ran forward, and never negative when it ran
|
|
37
|
+
* back. No samples, or none apart in time, is no speed.
|
|
37
38
|
*/
|
|
38
39
|
function sampleSpeed(samples) {
|
|
39
|
-
|
|
40
|
+
if (samples.length === 0)
|
|
41
|
+
return undefined;
|
|
42
|
+
const times = samples.map((sample) => sample.timestamp);
|
|
43
|
+
const earliest = Math.min(...times);
|
|
44
|
+
const span = Math.max(...times) - earliest;
|
|
40
45
|
if (span === 0)
|
|
41
46
|
return undefined;
|
|
42
|
-
|
|
47
|
+
const moved = samples.reduce((sum, sample) => sum + sample.completed, 0) - samples[times.indexOf(earliest)].completed;
|
|
48
|
+
return moved / span;
|
|
43
49
|
}
|
|
44
50
|
/**
|
|
45
51
|
* A count as a task takes it in. [LAW:single-enforcer] `addTask` and
|
|
@@ -52,6 +58,25 @@ function finiteCount(field, value) {
|
|
|
52
58
|
throw new RangeError(`progress task ${field} must be a finite number, got ${value}`);
|
|
53
59
|
return value;
|
|
54
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* `state` as of `now`, the moment a frame is drawn for: a host whose timeline
|
|
63
|
+
* runs back draws moments before what was stamped later, so a task drawn
|
|
64
|
+
* before its start has not started, one drawn before it reached its total
|
|
65
|
+
* has not finished — and finishes no sooner than it started — and its speed
|
|
66
|
+
* is taken over the updates that had come by then.
|
|
67
|
+
*/
|
|
68
|
+
function taskAt({ samples, finishedAt, ...state }, now) {
|
|
69
|
+
const startTime = state.startTime !== undefined && now >= state.startTime ? state.startTime : undefined;
|
|
70
|
+
const finishedTime = startTime !== undefined && finishedAt !== undefined && now >= finishedAt ? Math.max(0, finishedAt - startTime) : undefined;
|
|
71
|
+
const started = startTime !== undefined;
|
|
72
|
+
return {
|
|
73
|
+
...state,
|
|
74
|
+
startTime,
|
|
75
|
+
elapsed: started ? now - startTime : undefined,
|
|
76
|
+
finishedTime,
|
|
77
|
+
speed: started ? sampleSpeed(samples.filter((sample) => sample.timestamp <= now)) : undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
55
80
|
/** Rich's `Task.finished`. [LAW:one-source-of-truth] The one predicate every reader of "finished" asks. */
|
|
56
81
|
function finished(task) {
|
|
57
82
|
return task.finishedTime !== undefined;
|
|
@@ -83,15 +108,22 @@ export class BarColumn {
|
|
|
83
108
|
constructor(barWidth) {
|
|
84
109
|
this.barWidth = barWidth ?? 40;
|
|
85
110
|
}
|
|
86
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Rich's `BarColumn`: a task not yet started, or with no total to fill
|
|
113
|
+
* toward, pulses, and a pulse draws no fill. Otherwise the bar is the
|
|
114
|
+
* task's `fill` drawn at the frame's time, every cell stamped with when the
|
|
115
|
+
* fill next moves, so a host draws while it eases and the bar asks for
|
|
116
|
+
* nothing once it lands. Its colour is the task's: an ease that overshoots
|
|
117
|
+
* the total does not finish the bar, and one that dips back does not
|
|
118
|
+
* unfinish it.
|
|
119
|
+
*/
|
|
87
120
|
render(task) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
total: task.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
121
|
+
const total = task.total ?? 100;
|
|
122
|
+
if (task.startTime === undefined || task.total === undefined) {
|
|
123
|
+
return new ProgressBar({ total, completed: task.completed, width: this.barWidth, pulse: true });
|
|
124
|
+
}
|
|
125
|
+
const done = task.completed >= total;
|
|
126
|
+
return Animation.map(task.fill, (completed) => new ProgressBar({ total, completed, width: this.barWidth, finished: done }));
|
|
95
127
|
}
|
|
96
128
|
}
|
|
97
129
|
/**
|
|
@@ -151,11 +183,21 @@ export class TimeElapsedColumn {
|
|
|
151
183
|
tableColumn = {};
|
|
152
184
|
render(task) {
|
|
153
185
|
// Rich's `finished_time if finished else elapsed`: a finished task's clock
|
|
154
|
-
// holds where it stopped, and one never started has no time to show.
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
// holds where it stopped, and one never started has no time to show. A
|
|
187
|
+
// running one is a clock placed in the cell, so the frame says when its
|
|
188
|
+
// next second turns and a host paints it then. [LAW:one-source-of-truth]
|
|
189
|
+
if (finished(task))
|
|
190
|
+
return elapsedFace(task.finishedTime);
|
|
191
|
+
return task.startTime === undefined ? elapsedFace(undefined) : runningClock(task.startTime);
|
|
157
192
|
}
|
|
158
193
|
}
|
|
194
|
+
const elapsedFace = (seconds) => new RichText(clockFace(seconds), { style: "progress.elapsed" });
|
|
195
|
+
/**
|
|
196
|
+
* The seconds since `startTime` at the frame's time, turning over on each
|
|
197
|
+
* whole one. A clock is information and not motion, so reduced it still
|
|
198
|
+
* keeps the time: `Animation.repeat` keeps its turns.
|
|
199
|
+
*/
|
|
200
|
+
const runningClock = (startTime) => Timeline.run(Timeline.play(startTime), Animation.repeat(1, (second) => Animation.constant(elapsedFace(second))));
|
|
159
201
|
/** A time as the time columns draw it, `-:--:--` when there is none to draw. */
|
|
160
202
|
function clockFace(seconds) {
|
|
161
203
|
return seconds === undefined ? "-:--:--" : formatTime(seconds);
|
|
@@ -174,10 +216,10 @@ export class SpinnerColumn {
|
|
|
174
216
|
const finishedText = options?.finishedText ?? " ";
|
|
175
217
|
this._finishedText = typeof finishedText === "string" ? readStr(finishedText, true) : finishedText.copy();
|
|
176
218
|
}
|
|
177
|
-
// Rich's `self.spinner.render(task.get_time())`:
|
|
178
|
-
//
|
|
219
|
+
// Rich's `self.spinner.render(task.get_time())`: placed, the spinner draws
|
|
220
|
+
// at the frame's time, which is the time the task is drawn at.
|
|
179
221
|
render(task) {
|
|
180
|
-
return finished(task) ? this._finishedText : this._spinner
|
|
222
|
+
return finished(task) ? this._finishedText : this._spinner;
|
|
181
223
|
}
|
|
182
224
|
}
|
|
183
225
|
/**
|
|
@@ -220,11 +262,20 @@ function formatTime(seconds) {
|
|
|
220
262
|
}
|
|
221
263
|
export class Progress {
|
|
222
264
|
_columns;
|
|
223
|
-
|
|
224
|
-
|
|
265
|
+
// Rich's Progress adds and updates tasks between frames: each task is a
|
|
266
|
+
// record replaced whole on update, so a frame reads one task as it stood.
|
|
267
|
+
// A task's id is its place in the list, from 1, so an update finds it
|
|
268
|
+
// without copying the others.
|
|
269
|
+
_tasks = new ListState();
|
|
270
|
+
// [LAW:types-are-the-program] Its own Live, or none: a Progress another
|
|
271
|
+
// host draws has nothing of its own to start.
|
|
225
272
|
_live;
|
|
226
|
-
|
|
227
|
-
|
|
273
|
+
// [LAW:one-source-of-truth] The one source of every time this display
|
|
274
|
+
// keeps or draws at, so a task's stamps and the frames it is drawn in are
|
|
275
|
+
// readings of one host.
|
|
276
|
+
_host;
|
|
277
|
+
/** A task's fill after its count changes at `now`, from the value it shows then. */
|
|
278
|
+
_refill;
|
|
228
279
|
expand;
|
|
229
280
|
constructor(...columns) {
|
|
230
281
|
// Last arg might be options
|
|
@@ -242,25 +293,37 @@ export class Progress {
|
|
|
242
293
|
cols.push(new TextColumn("[progress.description]{task.description}"), new BarColumn(), new TaskProgressColumn(), new TimeRemainingColumn());
|
|
243
294
|
}
|
|
244
295
|
this._columns = cols;
|
|
245
|
-
this._tasks = new Map();
|
|
246
|
-
this._nextId = 1;
|
|
247
|
-
this._console = opts.console ?? new Console();
|
|
248
296
|
this.expand = opts.expand ?? false;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
297
|
+
// [LAW:dataflow-not-control-flow] The option picks the rule once; an
|
|
298
|
+
// update always refills through it. Eased, the tween starts at the
|
|
299
|
+
// update's moment, and the fill it replaces is never asked for again, so
|
|
300
|
+
// a task keeps one animation, not its history: a frame drawn for a
|
|
301
|
+
// moment before the update — a timeline run back — finds the new tween
|
|
302
|
+
// not yet begun, at the value the old one showed.
|
|
303
|
+
const timing = opts.fill;
|
|
304
|
+
this._refill = timing === undefined ? (_from, to) => Animation.constant(to) : (from, to, now) => Timeline.run(Timeline.play(now), tween(from, to, timing));
|
|
305
|
+
if (opts.drawnBy === undefined) {
|
|
306
|
+
this._live = new Live(this, {
|
|
307
|
+
console: opts.console,
|
|
308
|
+
clock: opts.clock,
|
|
309
|
+
refreshPerSecond: opts.refreshPerSecond ?? 10,
|
|
310
|
+
autoRefresh: opts.autoRefresh,
|
|
311
|
+
transient: opts.transient,
|
|
312
|
+
});
|
|
313
|
+
this._host = this._live;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this._live = undefined;
|
|
317
|
+
this._host = opts.drawnBy;
|
|
318
|
+
}
|
|
257
319
|
}
|
|
320
|
+
/** The console its own `Live` prints above the bars through. */
|
|
258
321
|
get console() {
|
|
259
|
-
return this.
|
|
322
|
+
return this._ownLive("console").console;
|
|
260
323
|
}
|
|
261
324
|
/** Rich's `Progress.finished`: every task finished, and so true with none. */
|
|
262
325
|
get finished() {
|
|
263
|
-
return
|
|
326
|
+
return this._tasks.get().every((task) => finished(taskAt(task.get(), this._host.frameTime)));
|
|
264
327
|
}
|
|
265
328
|
static getDefaultColumns() {
|
|
266
329
|
return [
|
|
@@ -271,91 +334,115 @@ export class Progress {
|
|
|
271
334
|
];
|
|
272
335
|
}
|
|
273
336
|
addTask(description, options) {
|
|
274
|
-
const id = this.
|
|
275
|
-
|
|
337
|
+
const id = this._tasks.get().length + 1;
|
|
338
|
+
this._tasks.push(new State({
|
|
276
339
|
id,
|
|
277
340
|
description,
|
|
278
341
|
total: options?.total === undefined ? undefined : finiteCount("total", options.total),
|
|
279
342
|
completed: 0,
|
|
343
|
+
fill: Animation.constant(0),
|
|
280
344
|
visible: options?.visible !== false,
|
|
281
|
-
startTime: options?.start === false ? undefined : this.
|
|
282
|
-
|
|
345
|
+
startTime: options?.start === false ? undefined : this._host.frameTime,
|
|
346
|
+
finishedAt: undefined,
|
|
283
347
|
samples: [],
|
|
284
|
-
};
|
|
285
|
-
this._tasks.set(id, task);
|
|
348
|
+
}));
|
|
286
349
|
return id;
|
|
287
350
|
}
|
|
351
|
+
_task(taskId) {
|
|
352
|
+
return this._tasks.get()[taskId - 1];
|
|
353
|
+
}
|
|
288
354
|
updateTask(taskId, options) {
|
|
289
|
-
const
|
|
290
|
-
if (!
|
|
355
|
+
const held = this._task(taskId);
|
|
356
|
+
if (!held)
|
|
291
357
|
return;
|
|
358
|
+
const task = held.get();
|
|
292
359
|
// Rich's order: `advance` is added, then a given `completed` overwrites it,
|
|
293
360
|
// so `completed` wins when both come. Checked whole before any field is
|
|
294
361
|
// written, so a refused update leaves the task as it was.
|
|
362
|
+
const live = options.refresh ? this._ownLive("refresh") : undefined;
|
|
295
363
|
const advance = finiteCount("advance", options.advance ?? 0);
|
|
296
364
|
const completed = options.completed === undefined
|
|
297
365
|
? finiteCount("completed + advance", task.completed + advance)
|
|
298
366
|
: finiteCount("completed", options.completed);
|
|
299
|
-
const now = this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
task.completed
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
367
|
+
const now = this._host.frameTime;
|
|
368
|
+
const reached = task.startTime !== undefined && task.total !== undefined && completed >= task.total;
|
|
369
|
+
held.set({
|
|
370
|
+
...task,
|
|
371
|
+
completed,
|
|
372
|
+
// A retarget is a change of count: an update that leaves it alone leaves
|
|
373
|
+
// the ease running as it was, rather than restarting it from where it is.
|
|
374
|
+
fill: completed === task.completed ? task.fill : this._refill(task.fill.at(now), completed, now),
|
|
375
|
+
description: options.description ?? task.description,
|
|
376
|
+
visible: options.visible ?? task.visible,
|
|
377
|
+
samples: recordSample(task.samples, now, completed - task.completed),
|
|
378
|
+
// Rich's `finished_time`, kept as the moment it was reached, so a frame
|
|
379
|
+
// drawn before it — a timeline run back — finds the task not yet finished.
|
|
380
|
+
finishedAt: task.finishedAt ?? (reached ? now : undefined),
|
|
381
|
+
});
|
|
382
|
+
live?.refresh();
|
|
315
383
|
}
|
|
316
384
|
startTask(taskId) {
|
|
317
|
-
const
|
|
385
|
+
const held = this._task(taskId);
|
|
318
386
|
// Rich's `start_task`: a task already running keeps the time it started.
|
|
319
|
-
if (
|
|
320
|
-
|
|
387
|
+
if (held)
|
|
388
|
+
held.set({ ...held.get(), startTime: held.get().startTime ?? this._host.frameTime });
|
|
321
389
|
}
|
|
322
390
|
start() {
|
|
323
|
-
this.
|
|
391
|
+
this._ownLive("start").start();
|
|
324
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Every bar settles at its count, so the frame drawn next — its own `Live`'s
|
|
395
|
+
* final one, handed back here, or the next of the host that draws it —
|
|
396
|
+
* shows where the work got to, not a bar caught mid-ease.
|
|
397
|
+
*/
|
|
325
398
|
stop() {
|
|
326
|
-
this.
|
|
399
|
+
for (const task of this._tasks.get())
|
|
400
|
+
task.set({ ...task.get(), fill: Animation.constant(task.get().completed) });
|
|
401
|
+
this._live?.stop();
|
|
327
402
|
}
|
|
328
403
|
refresh() {
|
|
329
|
-
this.
|
|
404
|
+
this._ownLive("refresh").refresh();
|
|
405
|
+
}
|
|
406
|
+
_ownLive(asked) {
|
|
407
|
+
if (this._live === undefined) {
|
|
408
|
+
throw new Error(`Progress.${asked}: this Progress has no Live of its own; it is drawn by the host it was given as drawnBy, so use that host's ${asked}`);
|
|
409
|
+
}
|
|
410
|
+
return this._live;
|
|
330
411
|
}
|
|
331
412
|
*render(options) {
|
|
332
|
-
|
|
413
|
+
const framed = this._framed(options);
|
|
414
|
+
yield* render(this._table(framed.frame.time), framed);
|
|
333
415
|
}
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
416
|
+
// Inside a frame, the time its host read for it: a second read here would
|
|
417
|
+
// land a moving bar's stamp a hair after the frame, a moment the host takes
|
|
418
|
+
// for one still to come. A frame is only ever this display's host's,
|
|
419
|
+
// because the tasks were stamped on that host's time and a frame of any
|
|
420
|
+
// other is drawn for a moment on a different clock. A print is drawn for no
|
|
421
|
+
// moment, so it is drawn in a frame for the one the program asks at, read
|
|
422
|
+
// as `updateTask` reads it, and every bar and spinner placed in the table
|
|
423
|
+
// draws at that moment too. [LAW:one-source-of-truth]
|
|
424
|
+
_framed(options) {
|
|
425
|
+
const { frame } = options;
|
|
426
|
+
if (frame !== undefined && frame.host !== this._host) {
|
|
427
|
+
throw new Error("a Progress was drawn in a frame of a host other than its own; give it that host as `drawnBy`");
|
|
428
|
+
}
|
|
429
|
+
return { ...options, frame: frame ?? { time: this._host.frameTime, host: this._host } };
|
|
338
430
|
}
|
|
339
|
-
// Rich's `make_tasks_table`: each column laid out as it says
|
|
340
|
-
|
|
431
|
+
// Rich's `make_tasks_table`: each column laid out as it says, at `now` on
|
|
432
|
+
// this display's clock.
|
|
433
|
+
_table(now) {
|
|
341
434
|
const table = Table.grid({ expand: this.expand });
|
|
342
435
|
for (const col of this._columns) {
|
|
343
436
|
table.addColumn(undefined, col.tableColumn);
|
|
344
437
|
}
|
|
345
|
-
// [LAW:effects-at-boundaries] One
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
for (const
|
|
438
|
+
// [LAW:effects-at-boundaries] One moment a frame, handed to every column
|
|
439
|
+
// as the task's `elapsed`; a column that moves places an animation, which
|
|
440
|
+
// draws at that same moment, the frame's.
|
|
441
|
+
for (const held of this._tasks.get()) {
|
|
442
|
+
const state = held.get();
|
|
349
443
|
if (!state.visible)
|
|
350
444
|
continue;
|
|
351
|
-
const
|
|
352
|
-
const started = startTime !== undefined;
|
|
353
|
-
const task = {
|
|
354
|
-
...state,
|
|
355
|
-
time: now,
|
|
356
|
-
elapsed: started ? now - startTime : undefined,
|
|
357
|
-
speed: started ? sampleSpeed(samples) : undefined,
|
|
358
|
-
};
|
|
445
|
+
const task = taskAt(state, now);
|
|
359
446
|
table.addRow(...this._columns.map((col) => col.render(task)));
|
|
360
447
|
}
|
|
361
448
|
return table;
|
|
@@ -9,29 +9,39 @@ export interface ProgressBarOptions {
|
|
|
9
9
|
completed?: number;
|
|
10
10
|
width?: number;
|
|
11
11
|
/**
|
|
12
|
-
* Rich's pulse, for work whose end is not known
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Rich's pulse, for work whose end is not known: the whole bar in its back
|
|
13
|
+
* style, with the library's `shimmer` carrying `bar.pulse` light along it,
|
|
14
|
+
* drawn at the time of the frame it is drawn in. Without it, the bar shows
|
|
15
|
+
* `completed` of `total`.
|
|
16
16
|
*/
|
|
17
|
-
pulse?:
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
pulse?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the work is done, which picks `finishedStyle` over
|
|
20
|
+
* `completeStyle`: `completed` reaching `total`, unless said. A bar drawing
|
|
21
|
+
* an eased fill says it, since the fill it draws is not the count.
|
|
22
|
+
*/
|
|
23
|
+
finished?: boolean;
|
|
20
24
|
style?: string | Style;
|
|
21
25
|
completeStyle?: string | Style;
|
|
22
26
|
finishedStyle?: string | Style;
|
|
23
27
|
}
|
|
24
28
|
export declare class ProgressBar implements Renderable, Measurable {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
private readonly _total;
|
|
30
|
+
private readonly _completed;
|
|
31
|
+
private readonly _finished;
|
|
27
32
|
readonly width: number | undefined;
|
|
28
|
-
readonly pulse:
|
|
29
|
-
readonly t: number;
|
|
30
|
-
} | undefined;
|
|
33
|
+
readonly pulse: boolean;
|
|
31
34
|
readonly style: string | Style;
|
|
32
35
|
readonly completeStyle: string | Style;
|
|
33
36
|
readonly finishedStyle: string | Style;
|
|
34
37
|
constructor(options?: ProgressBarOptions);
|
|
38
|
+
get total(): number;
|
|
39
|
+
set total(value: number);
|
|
40
|
+
get completed(): number;
|
|
41
|
+
set completed(value: number);
|
|
42
|
+
/** Whether the work is done, as given; unset, `completed` reaching `total` at the moment the bar is drawn. */
|
|
43
|
+
get finished(): boolean | undefined;
|
|
44
|
+
set finished(value: boolean | undefined);
|
|
35
45
|
render(options: RenderOptions): Iterable<Segment>;
|
|
36
46
|
/**
|
|
37
47
|
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
@@ -8,6 +8,7 @@ import { Style } from "../core/style.js";
|
|
|
8
8
|
import { drawable, getStyle, render } from "../core/protocol.js";
|
|
9
9
|
import { Effected } from "./effect.js";
|
|
10
10
|
import { EFFECT_CURVES, atShares, shimmer } from "./effects.js";
|
|
11
|
+
import { State } from "../core/state.js";
|
|
11
12
|
const BAR = "━";
|
|
12
13
|
const ASCII_BAR = "-";
|
|
13
14
|
const HALF_BAR_RIGHT = "╸";
|
|
@@ -22,27 +23,49 @@ const PULSE_REACH = 10;
|
|
|
22
23
|
/** Rich's pulse back where the back style names no colour. */
|
|
23
24
|
const PULSE_BACK = ColorSpec.fromRgba(new ColorRgba(0, 0, 0));
|
|
24
25
|
export class ProgressBar {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Rich's ProgressBar is updated after it is built: `bar.completed = n`.
|
|
27
|
+
_total;
|
|
28
|
+
_completed;
|
|
29
|
+
_finished;
|
|
27
30
|
width;
|
|
28
31
|
pulse;
|
|
29
32
|
style;
|
|
30
33
|
completeStyle;
|
|
31
34
|
finishedStyle;
|
|
32
35
|
constructor(options) {
|
|
33
|
-
this.
|
|
34
|
-
this.
|
|
36
|
+
this._total = new State(options?.total ?? 100);
|
|
37
|
+
this._completed = new State(options?.completed ?? 0);
|
|
35
38
|
this.width = options?.width;
|
|
36
|
-
this.pulse = options?.pulse;
|
|
39
|
+
this.pulse = options?.pulse ?? false;
|
|
40
|
+
this._finished = new State(options?.finished);
|
|
37
41
|
this.style = options?.style ?? "bar.back";
|
|
38
42
|
this.completeStyle = options?.completeStyle ?? "bar.complete";
|
|
39
43
|
this.finishedStyle = options?.finishedStyle ?? "bar.finished";
|
|
40
44
|
}
|
|
45
|
+
get total() {
|
|
46
|
+
return this._total.get();
|
|
47
|
+
}
|
|
48
|
+
set total(value) {
|
|
49
|
+
this._total.set(value);
|
|
50
|
+
}
|
|
51
|
+
get completed() {
|
|
52
|
+
return this._completed.get();
|
|
53
|
+
}
|
|
54
|
+
set completed(value) {
|
|
55
|
+
this._completed.set(value);
|
|
56
|
+
}
|
|
57
|
+
/** Whether the work is done, as given; unset, `completed` reaching `total` at the moment the bar is drawn. */
|
|
58
|
+
get finished() {
|
|
59
|
+
return this._finished.get();
|
|
60
|
+
}
|
|
61
|
+
set finished(value) {
|
|
62
|
+
this._finished.set(value);
|
|
63
|
+
}
|
|
41
64
|
*render(options) {
|
|
42
65
|
// Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
|
|
43
66
|
// [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
|
|
44
67
|
const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
|
|
45
|
-
if (this.pulse
|
|
68
|
+
if (!this.pulse) {
|
|
46
69
|
yield* this.track(options, width, this.completeHalves(width), getStyle(options, this.style));
|
|
47
70
|
return;
|
|
48
71
|
}
|
|
@@ -56,10 +79,10 @@ export class ProgressBar {
|
|
|
56
79
|
const style = getStyle(options, this.style);
|
|
57
80
|
const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(theme, true));
|
|
58
81
|
const back = style.add(Style.fromColor(ink));
|
|
59
|
-
// [LAW:no-ambient-temporal-coupling] The moment is the
|
|
82
|
+
// [LAW:no-ambient-temporal-coupling] The moment is the frame's; the bar
|
|
60
83
|
// only says what it looks like then. An empty track, lit by the shimmer.
|
|
61
84
|
const track = { render: (o) => this.track(o, width, 0, back) };
|
|
62
|
-
yield* render(new Effected(track, this.pulseEffect(options, back, depth, theme), {
|
|
85
|
+
yield* render(new Effected(track, this.pulseEffect(options, back, depth, theme), { key: "progress-bar", theme }), options);
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
65
88
|
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
@@ -82,7 +105,7 @@ export class ProgressBar {
|
|
|
82
105
|
*track(options, width, completeHalves, back) {
|
|
83
106
|
const barCount = Math.floor(completeHalves / 2);
|
|
84
107
|
const halfBarCount = completeHalves % 2;
|
|
85
|
-
const isFinished = this.completed >= this.total;
|
|
108
|
+
const isFinished = this.finished ?? this.completed >= this.total;
|
|
86
109
|
const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
|
|
87
110
|
const fillStyle = fill.isNull ? undefined : fill;
|
|
88
111
|
const backStyle = back.isNull ? undefined : back;
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { Console } from "../core/console.js";
|
|
25
25
|
import { renderMarkup } from "../core/markup.js";
|
|
26
26
|
import { RichText } from "../core/text.js";
|
|
27
|
+
import { pattern } from "../core/pattern.js";
|
|
27
28
|
/** An answer the prompt refused, and the markup it prints before asking again — Rich's `InvalidResponse`. */
|
|
28
29
|
class InvalidResponse {
|
|
29
30
|
message;
|
|
@@ -104,10 +105,10 @@ function responseKind(convert, invalid, options) {
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
/** 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 INTEGER = pattern(/^[+-]?\d(?:_?\d)*$/);
|
|
108
109
|
const DIGITS = String.raw `\d(?:_?\d)*`;
|
|
109
110
|
/** 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
|
+
const FLOAT = pattern(new RegExp(String.raw `^[+-]?(?:(?:${DIGITS}(?:\.(?:${DIGITS})?)?|\.${DIGITS})(?:e[+-]?${DIGITS})?|inf(?:inity)?|nan)$`, "i"));
|
|
111
112
|
/**
|
|
112
113
|
* The integer an answer spells, as Python's `int()` reads it, or none. One a
|
|
113
114
|
* `number` cannot hold exactly is none: Python's int is unbounded and a
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* JavaScript's: `\s` and `trim` take U+FEFF, which Python does not, and leave
|
|
6
6
|
* U+001C–U+001F and U+0085, which Python takes.
|
|
7
7
|
*/
|
|
8
|
+
import { pattern } from "../core/pattern.js";
|
|
8
9
|
/** The set as a regex character class, every member of it a single UTF-16 unit, for composing into a pattern where markdown-it-py's has `\s`. */
|
|
9
10
|
export const PY_WHITESPACE = String.raw `[\t-\r\x1c-\x20\x85\xa0 -
]`;
|
|
10
|
-
const PY_SPACE = new RegExp(`^${PY_WHITESPACE}$`);
|
|
11
|
+
const PY_SPACE = pattern(new RegExp(`^${PY_WHITESPACE}$`));
|
|
11
12
|
/** Python's `str.strip`, a scan in from each end, so a run of interior whitespace costs one pass, as it does there. */
|
|
12
13
|
export function pyStrip(text) {
|
|
13
14
|
let start = 0;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll — a slot of fixed size over content wider or taller than it, the
|
|
3
|
+
* content's offset in the slot moving with time: a long line panning as a
|
|
4
|
+
* marquee, a log tail scrolling, a panel sliding into its place.
|
|
5
|
+
*
|
|
6
|
+
* [LAW:one-type-per-behavior] A marquee, a scroll and a slide are one window
|
|
7
|
+
* and differ only in the motion handed in: the offset along each axis is an
|
|
8
|
+
* `Animation<number>`, so a marquee is a looping `Phase` scaled to the span
|
|
9
|
+
* it crosses and a slide-in is a `tween` to 0, and when the window next
|
|
10
|
+
* changes is `Animation.all`'s answer over the two. Nothing here knows which
|
|
11
|
+
* one it is drawing.
|
|
12
|
+
*
|
|
13
|
+
* The slot is what the window is. It measures as its width whatever the
|
|
14
|
+
* content, and draws exactly its rows of its width at every offset — cropped
|
|
15
|
+
* only to a narrower room it is given — so what sits beside it never moves.
|
|
16
|
+
* An offset before or past the content shows blank cells rather than stopping
|
|
17
|
+
* at an edge: that is what lets a marquee enter from the right and a slide-in
|
|
18
|
+
* arrive from outside the slot.
|
|
19
|
+
*
|
|
20
|
+
* The content is drawn at its own natural width and no height, its own
|
|
21
|
+
* length, because the content wider and taller than the slot is the thing
|
|
22
|
+
* being windowed. Content that should wrap to the slot instead, or that has
|
|
23
|
+
* no natural width because it fills whatever it is given, is
|
|
24
|
+
* `new Constrain(content, width)`, composed rather than asked for.
|
|
25
|
+
*
|
|
26
|
+
* [LAW:single-enforcer] A row is cut at the window's edges by
|
|
27
|
+
* `Segment.divide`, the one place a line is cut at a column: a wide glyph a
|
|
28
|
+
* cut falls in is blanked, never halved, and each cell's anchor still names
|
|
29
|
+
* its column in the content, so hit-testing finds the owner through a moving
|
|
30
|
+
* window.
|
|
31
|
+
*
|
|
32
|
+
* [LAW:no-ambient-temporal-coupling] Pure in `t`, nothing kept between
|
|
33
|
+
* frames; `at` hands over the frame alone, and placed, the scroll stamps
|
|
34
|
+
* the hold it is in at the frame's time (`placedRender`).
|
|
35
|
+
*/
|
|
36
|
+
import { Animation } from "../core/animation.js";
|
|
37
|
+
import type { Renderable } from "../core/protocol.js";
|
|
38
|
+
/** The slot a scroll draws, and how far the content is moved in it. */
|
|
39
|
+
export interface ScrollOptions {
|
|
40
|
+
/** The slot's width in cells. */
|
|
41
|
+
readonly width: number;
|
|
42
|
+
/** The slot's height in rows. */
|
|
43
|
+
readonly height: number;
|
|
44
|
+
/** Columns of content left of the slot's first, rounded to the nearest cell: negative moves the content right. Absent, 0. */
|
|
45
|
+
readonly col?: Animation<number>;
|
|
46
|
+
/** Rows of content above the slot's first, rounded to the nearest row: negative moves the content down. Absent, 0. */
|
|
47
|
+
readonly row?: Animation<number>;
|
|
48
|
+
}
|
|
49
|
+
/** `content` seen through a slot of `width` by `height` cells, moved in it by `col` and `row` at each moment. */
|
|
50
|
+
export declare function scroll(content: Renderable, options: ScrollOptions): Animation<Renderable>;
|