@promptctl/rich-js 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -5,10 +5,11 @@ import type { Segment } from "../core/segment.js";
5
5
  import { RichText } from "../core/text.js";
6
6
  import { Console } from "../core/console.js";
7
7
  import type { Style } from "../core/style.js";
8
- import { ProgressBar } from "./progressBar.js";
9
- import { type Clock } from "../core/clock.js";
8
+ import type { Clock } from "../core/clock.js";
10
9
  import { type ColumnOptions } from "./table.js";
11
- import type { Renderable, RenderOptions } from "../core/protocol.js";
10
+ import type { FrameHost, Renderable, RenderOptions } from "../core/protocol.js";
11
+ import { Animation } from "../core/animation.js";
12
+ import type { Timing } from "../core/easing.js";
12
13
  export interface TaskOptions {
13
14
  total?: number;
14
15
  start?: boolean;
@@ -22,30 +23,31 @@ export interface TaskUpdateOptions {
22
23
  refresh?: boolean;
23
24
  }
24
25
  export interface Task {
25
- id: number;
26
- description: string;
27
- total: number | undefined;
28
- completed: number;
29
- visible: boolean;
30
- /** When the task started, in seconds on its `Progress`'s clock; undefined until it has. */
31
- startTime: number | undefined;
26
+ readonly id: number;
27
+ readonly description: string;
28
+ readonly total: number | undefined;
29
+ readonly completed: number;
32
30
  /**
33
- * Rich's `Task.get_time()`: the moment the frame being drawn is drawn at, in
34
- * seconds on its `Progress`'s clock, so a column animates without reading one.
31
+ * The count a bar shows, over its `Progress`'s clock: `completed` itself,
32
+ * or moving to it after each update when the `Progress` was given a `fill`
33
+ * timing. `completed` stays the count; only the bar draws this.
35
34
  */
36
- time: number;
35
+ readonly fill: Animation<number>;
36
+ readonly visible: boolean;
37
+ /** When the task started, in seconds on its `Progress`'s host's time; undefined until it has, as of the frame being drawn. */
38
+ readonly startTime: number | undefined;
37
39
  /**
38
40
  * Rich's `Task.elapsed`: seconds since the task started, as of the frame
39
- * being drawn — `Progress` reads its clock once a frame and hands columns
41
+ * being drawn — `Progress` takes the time once a frame and hands columns
40
42
  * the result, so no column reads a clock. Undefined until the task starts.
41
43
  */
42
- elapsed: number | undefined;
44
+ readonly elapsed: number | undefined;
43
45
  /**
44
46
  * Rich's `Task.finished_time`: the elapsed time the first time an update
45
47
  * finds a started task at its total, kept from then on, so a finished task
46
48
  * stays finished however its count moves after.
47
49
  */
48
- finishedTime: number | undefined;
50
+ readonly finishedTime: number | undefined;
49
51
  /**
50
52
  * Rich's `Task.speed`: steps a second over the updates that moved the task
51
53
  * in the `SPEED_ESTIMATE_PERIOD` before its latest update — measured back from
@@ -53,7 +55,7 @@ export interface Task {
53
55
  * speed it had. Undefined until the task has started and two such updates lie
54
56
  * apart in time.
55
57
  */
56
- speed: number | undefined;
58
+ readonly speed: number | undefined;
57
59
  }
58
60
  /**
59
61
  * One cell of a `Progress` row, made for the task on that row. Not a
@@ -82,8 +84,16 @@ export declare class BarColumn implements ProgressColumn {
82
84
  readonly barWidth: number;
83
85
  readonly tableColumn: ColumnOptions;
84
86
  constructor(barWidth?: number);
85
- /** Rich's `BarColumn`: a task not yet started, or with no total to fill toward, pulses. */
86
- render(task: Task): ProgressBar;
87
+ /**
88
+ * Rich's `BarColumn`: a task not yet started, or with no total to fill
89
+ * toward, pulses, and a pulse draws no fill. Otherwise the bar is the
90
+ * task's `fill` drawn at the frame's time, every cell stamped with when the
91
+ * fill next moves, so a host draws while it eases and the bar asks for
92
+ * nothing once it lands. Its colour is the task's: an ease that overshoots
93
+ * the total does not finish the bar, and one that dips back does not
94
+ * unfinish it.
95
+ */
96
+ render(task: Task): Renderable;
87
97
  }
88
98
  /**
89
99
  * Rich's `{task.percentage:>3.0f}%`: the percentage right-aligned in three
@@ -112,7 +122,7 @@ export declare class TimeRemainingColumn implements ProgressColumn {
112
122
  }
113
123
  export declare class TimeElapsedColumn implements ProgressColumn {
114
124
  readonly tableColumn: ColumnOptions;
115
- render(task: Task): RichText;
125
+ render(task: Task): Renderable;
116
126
  }
117
127
  export interface SpinnerColumnOptions {
118
128
  /** The frame's style. Defaults to `progress.spinner`, a name its console's theme resolves. */
@@ -125,7 +135,7 @@ export interface SpinnerColumnOptions {
125
135
  export declare class SpinnerColumn implements ProgressColumn {
126
136
  readonly tableColumn: ColumnOptions;
127
137
  private readonly _spinner;
128
- private _finishedText;
138
+ private readonly _finishedText;
129
139
  constructor(spinnerName?: string, options?: SpinnerColumnOptions);
130
140
  render(task: Task): Renderable;
131
141
  }
@@ -144,41 +154,82 @@ export declare class MofNCompleteColumn implements ProgressColumn {
144
154
  constructor(options?: MofNCompleteColumnOptions);
145
155
  render(task: Task): RichText;
146
156
  }
147
- export interface ProgressOptions {
157
+ /**
158
+ * How a `Progress` is set up. It is drawn by one host, and every time it
159
+ * keeps or draws at is that host's `frameTime`: by its own `Live`, which the
160
+ * options for that `Live` configure, or, given `drawnBy`, by another host —
161
+ * which then does all the drawing, so none of them apply.
162
+ */
163
+ export type ProgressOptions = ProgressDisplayOptions & (OwnLiveOptions | DrawnByOptions);
164
+ interface ProgressDisplayOptions {
165
+ expand?: boolean;
166
+ /**
167
+ * How a bar's fill moves to a task's new count: eased over this timing
168
+ * from wherever it is when the count changes, so a bar advancing in bursts
169
+ * reads as motion. Unset, the bar jumps to the count, as Rich's does.
170
+ */
171
+ fill?: Timing;
172
+ }
173
+ interface OwnLiveOptions {
174
+ drawnBy?: undefined;
175
+ console?: Console;
148
176
  refreshPerSecond?: number;
149
177
  autoRefresh?: boolean;
150
178
  transient?: boolean;
151
- expand?: boolean;
152
- console?: Console;
153
179
  /**
154
- * What tasks are timed by and frames are drawn on, Rich's `get_time`;
155
- * defaults to the platform's monotonic clock. Handed on to the `Live`.
180
+ * What its own `Live` ticks on, and so what tasks are timed by — Rich's
181
+ * `get_time`; defaults to the platform's monotonic clock.
156
182
  */
157
183
  clock?: Clock;
158
184
  }
185
+ interface DrawnByOptions {
186
+ /**
187
+ * The host — a `Live`, an `App` — whose frames this `Progress` is drawn in:
188
+ * tasks are timed by its `frameTime`, so they hold while its timeline is
189
+ * paused and run back while it is reversed. Drawn in any other host's
190
+ * frame, the `Progress` throws.
191
+ */
192
+ drawnBy: FrameHost;
193
+ console?: never;
194
+ refreshPerSecond?: never;
195
+ autoRefresh?: never;
196
+ transient?: never;
197
+ clock?: never;
198
+ }
159
199
  export declare class Progress implements Renderable {
160
- private _columns;
161
- private _tasks;
162
- private _nextId;
163
- private _live;
164
- private _console;
165
- private readonly _clock;
200
+ private readonly _columns;
201
+ private readonly _tasks;
202
+ private readonly _live;
203
+ private readonly _host;
204
+ /** A task's fill after its count changes at `now`, from the value it shows then. */
205
+ private readonly _refill;
166
206
  readonly expand: boolean;
167
207
  constructor(...columns: (ProgressColumn | ProgressOptions)[]);
208
+ /** The console its own `Live` prints above the bars through. */
168
209
  get console(): Console;
169
210
  /** Rich's `Progress.finished`: every task finished, and so true with none. */
170
211
  get finished(): boolean;
171
212
  static getDefaultColumns(): ProgressColumn[];
172
213
  addTask(description: string, options?: TaskOptions): number;
214
+ private _task;
173
215
  updateTask(taskId: number, options: TaskUpdateOptions): void;
174
216
  startTask(taskId: number): void;
175
217
  start(): void;
218
+ /**
219
+ * Every bar settles at its count, so the frame drawn next — its own `Live`'s
220
+ * final one, handed back here, or the next of the host that draws it —
221
+ * shows where the work got to, not a bar caught mid-ease.
222
+ */
176
223
  stop(): void;
177
224
  refresh(): void;
225
+ private _ownLive;
178
226
  render(options: RenderOptions): Iterable<Segment>;
227
+ private _framed;
228
+ private _table;
179
229
  }
180
230
  export declare function track<T>(iterable: Iterable<T>, options?: {
181
231
  description?: string;
182
232
  total?: number;
183
233
  console?: Console;
184
234
  }): Iterable<T>;
235
+ export {};
@@ -2,13 +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";
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";
12
15
  /**
13
16
  * Rich's defaults for `speed_estimate_period` and its sample deque's
14
17
  * `maxlen`: an update forgets samples older than the period, and the newest
@@ -17,28 +20,32 @@ import { Table } from "./table.js";
17
20
  const SPEED_ESTIMATE_PERIOD = 30;
18
21
  const MAX_SAMPLES = 1000;
19
22
  /**
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.
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.
24
28
  */
25
29
  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);
30
+ const fresh = samples.filter((sample) => sample.timestamp >= now - SPEED_ESTIMATE_PERIOD);
31
+ return [...fresh, ...(moved > 0 ? [{ timestamp: now, completed: moved }] : [])].slice(-MAX_SAMPLES);
31
32
  }
32
33
  /**
33
34
  * 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.
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.
36
38
  */
37
39
  function sampleSpeed(samples) {
38
- const span = (samples.at(-1)?.timestamp ?? 0) - (samples[0]?.timestamp ?? 0);
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;
39
45
  if (span === 0)
40
46
  return undefined;
41
- return samples.slice(1).reduce((sum, sample) => sum + sample.completed, 0) / span;
47
+ const moved = samples.reduce((sum, sample) => sum + sample.completed, 0) - samples[times.indexOf(earliest)].completed;
48
+ return moved / span;
42
49
  }
43
50
  /**
44
51
  * A count as a task takes it in. [LAW:single-enforcer] `addTask` and
@@ -51,6 +58,25 @@ function finiteCount(field, value) {
51
58
  throw new RangeError(`progress task ${field} must be a finite number, got ${value}`);
52
59
  return value;
53
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
+ }
54
80
  /** Rich's `Task.finished`. [LAW:one-source-of-truth] The one predicate every reader of "finished" asks. */
55
81
  function finished(task) {
56
82
  return task.finishedTime !== undefined;
@@ -82,15 +108,22 @@ export class BarColumn {
82
108
  constructor(barWidth) {
83
109
  this.barWidth = barWidth ?? 40;
84
110
  }
85
- /** Rich's `BarColumn`: a task not yet started, or with no total to fill toward, pulses. */
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
+ */
86
120
  render(task) {
87
- const pulsing = task.startTime === undefined || task.total === undefined;
88
- return new ProgressBar({
89
- total: task.total ?? 100,
90
- completed: task.completed,
91
- width: this.barWidth,
92
- pulse: pulsing ? { t: task.time } : undefined,
93
- });
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 }));
94
127
  }
95
128
  }
96
129
  /**
@@ -150,11 +183,21 @@ export class TimeElapsedColumn {
150
183
  tableColumn = {};
151
184
  render(task) {
152
185
  // Rich's `finished_time if finished else elapsed`: a finished task's clock
153
- // holds where it stopped, and one never started has no time to show.
154
- const time = finished(task) ? task.finishedTime : task.elapsed;
155
- return new RichText(clockFace(time), { style: "progress.elapsed" });
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);
156
192
  }
157
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))));
158
201
  /** A time as the time columns draw it, `-:--:--` when there is none to draw. */
159
202
  function clockFace(seconds) {
160
203
  return seconds === undefined ? "-:--:--" : formatTime(seconds);
@@ -173,10 +216,10 @@ export class SpinnerColumn {
173
216
  const finishedText = options?.finishedText ?? " ";
174
217
  this._finishedText = typeof finishedText === "string" ? readStr(finishedText, true) : finishedText.copy();
175
218
  }
176
- // Rich's `self.spinner.render(task.get_time())`: the frame for the time the
177
- // task is drawn at, which `Progress` read off its clock once for the frame.
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.
178
221
  render(task) {
179
- return finished(task) ? this._finishedText : this._spinner.at(task.time);
222
+ return finished(task) ? this._finishedText : this._spinner;
180
223
  }
181
224
  }
182
225
  /**
@@ -219,11 +262,20 @@ function formatTime(seconds) {
219
262
  }
220
263
  export class Progress {
221
264
  _columns;
222
- _tasks;
223
- _nextId;
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.
224
272
  _live;
225
- _console;
226
- _clock;
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;
227
279
  expand;
228
280
  constructor(...columns) {
229
281
  // Last arg might be options
@@ -241,25 +293,37 @@ export class Progress {
241
293
  cols.push(new TextColumn("[progress.description]{task.description}"), new BarColumn(), new TaskProgressColumn(), new TimeRemainingColumn());
242
294
  }
243
295
  this._columns = cols;
244
- this._tasks = new Map();
245
- this._nextId = 1;
246
- this._console = opts.console ?? new Console();
247
296
  this.expand = opts.expand ?? false;
248
- this._clock = opts.clock ?? systemClock();
249
- this._live = new Live(this, {
250
- console: this._console,
251
- clock: this._clock,
252
- refreshPerSecond: opts.refreshPerSecond ?? 10,
253
- autoRefresh: opts.autoRefresh,
254
- transient: opts.transient,
255
- });
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
+ }
256
319
  }
320
+ /** The console its own `Live` prints above the bars through. */
257
321
  get console() {
258
- return this._console;
322
+ return this._ownLive("console").console;
259
323
  }
260
324
  /** Rich's `Progress.finished`: every task finished, and so true with none. */
261
325
  get finished() {
262
- return [...this._tasks.values()].every(finished);
326
+ return this._tasks.get().every((task) => finished(taskAt(task.get(), this._host.frameTime)));
263
327
  }
264
328
  static getDefaultColumns() {
265
329
  return [
@@ -270,86 +334,118 @@ export class Progress {
270
334
  ];
271
335
  }
272
336
  addTask(description, options) {
273
- const id = this._nextId++;
274
- const task = {
337
+ const id = this._tasks.get().length + 1;
338
+ this._tasks.push(new State({
275
339
  id,
276
340
  description,
277
341
  total: options?.total === undefined ? undefined : finiteCount("total", options.total),
278
342
  completed: 0,
343
+ fill: Animation.constant(0),
279
344
  visible: options?.visible !== false,
280
- startTime: options?.start === false ? undefined : this._clock.now(),
281
- finishedTime: undefined,
345
+ startTime: options?.start === false ? undefined : this._host.frameTime,
346
+ finishedAt: undefined,
282
347
  samples: [],
283
- };
284
- this._tasks.set(id, task);
348
+ }));
285
349
  return id;
286
350
  }
351
+ _task(taskId) {
352
+ return this._tasks.get()[taskId - 1];
353
+ }
287
354
  updateTask(taskId, options) {
288
- const task = this._tasks.get(taskId);
289
- if (!task)
355
+ const held = this._task(taskId);
356
+ if (!held)
290
357
  return;
358
+ const task = held.get();
291
359
  // Rich's order: `advance` is added, then a given `completed` overwrites it,
292
360
  // so `completed` wins when both come. Checked whole before any field is
293
361
  // written, so a refused update leaves the task as it was.
362
+ const live = options.refresh ? this._ownLive("refresh") : undefined;
294
363
  const advance = finiteCount("advance", options.advance ?? 0);
295
364
  const completed = options.completed === undefined
296
365
  ? finiteCount("completed + advance", task.completed + advance)
297
366
  : finiteCount("completed", options.completed);
298
- const now = this._clock.now();
299
- recordSample(task.samples, now, completed - task.completed);
300
- task.completed = completed;
301
- if (options.description !== undefined)
302
- task.description = options.description;
303
- if (options.visible !== undefined)
304
- task.visible = options.visible;
305
- if (!finished(task) &&
306
- task.startTime !== undefined &&
307
- task.total !== undefined &&
308
- task.completed >= task.total) {
309
- task.finishedTime = now - task.startTime;
310
- }
311
- if (options.refresh) {
312
- this._live.update(this, { refresh: true });
313
- }
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();
314
383
  }
315
384
  startTask(taskId) {
316
- const task = this._tasks.get(taskId);
385
+ const held = this._task(taskId);
317
386
  // Rich's `start_task`: a task already running keeps the time it started.
318
- if (task)
319
- task.startTime ??= this._clock.now();
387
+ if (held)
388
+ held.set({ ...held.get(), startTime: held.get().startTime ?? this._host.frameTime });
320
389
  }
321
390
  start() {
322
- this._live.start();
391
+ this._ownLive("start").start();
323
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
+ */
324
398
  stop() {
325
- this._live.stop();
399
+ for (const task of this._tasks.get())
400
+ task.set({ ...task.get(), fill: Animation.constant(task.get().completed) });
401
+ this._live?.stop();
326
402
  }
327
403
  refresh() {
328
- this._live.refresh();
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;
329
411
  }
330
412
  *render(options) {
331
- // Rich's `make_tasks_table`: each column laid out as it says.
413
+ const framed = this._framed(options);
414
+ yield* render(this._table(framed.frame.time), framed);
415
+ }
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 } };
430
+ }
431
+ // Rich's `make_tasks_table`: each column laid out as it says, at `now` on
432
+ // this display's clock.
433
+ _table(now) {
332
434
  const table = Table.grid({ expand: this.expand });
333
435
  for (const col of this._columns) {
334
436
  table.addColumn(undefined, col.tableColumn);
335
437
  }
336
- // [LAW:effects-at-boundaries] One read of the clock a frame, handed to
337
- // every column as the task's `time` and `elapsed`.
338
- const now = this._clock.now();
339
- for (const { samples, ...state } of this._tasks.values()) {
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();
340
443
  if (!state.visible)
341
444
  continue;
342
- const { startTime } = state;
343
- const started = startTime !== undefined;
344
- const task = {
345
- ...state,
346
- time: now,
347
- elapsed: started ? now - startTime : undefined,
348
- speed: started ? sampleSpeed(samples) : undefined,
349
- };
445
+ const task = taskAt(state, now);
350
446
  table.addRow(...this._columns.map((col) => col.render(task)));
351
447
  }
352
- yield* table.render(options);
448
+ return table;
353
449
  }
354
450
  }
355
451
  // --- track ---
@@ -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, drawn as it is `t` seconds
13
- * into whoever draws the frames' clock: the whole bar in its back style,
14
- * with the library's `shimmer` carrying `bar.pulse` light along it. Without
15
- * it, the bar shows `completed` of `total`.
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
- readonly t: number;
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
- total: number;
26
- completed: number;
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