@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.
Files changed (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +24 -33
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +98 -29
  47. package/dist/core/strip.js +211 -150
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +158 -63
  73. package/dist/renderables/effect.js +378 -155
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  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 +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. 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, in place: drop the
22
- * samples older than the period as of `now`, then add this update's if it
23
- * moved the task forward, keeping the newest `MAX_SAMPLES`. The samples are in
24
- * 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.
25
28
  */
26
29
  function recordSample(samples, now, moved) {
27
- const firstFresh = samples.findIndex((sample) => sample.timestamp >= now - SPEED_ESTIMATE_PERIOD);
28
- samples.splice(0, firstFresh === -1 ? samples.length : firstFresh);
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 first sample, over the time from the first to the last. No samples, or
36
- * 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.
37
38
  */
38
39
  function sampleSpeed(samples) {
39
- 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;
40
45
  if (span === 0)
41
46
  return undefined;
42
- 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;
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
- /** 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
+ */
87
120
  render(task) {
88
- const pulsing = task.startTime === undefined || task.total === undefined;
89
- return new ProgressBar({
90
- total: task.total ?? 100,
91
- completed: task.completed,
92
- width: this.barWidth,
93
- pulse: pulsing ? { t: task.time } : undefined,
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
- const time = finished(task) ? task.finishedTime : task.elapsed;
156
- 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);
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())`: the frame for the time the
178
- // 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.
179
221
  render(task) {
180
- return finished(task) ? this._finishedText : this._spinner.at(task.time);
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
- _tasks;
224
- _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.
225
272
  _live;
226
- _console;
227
- _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;
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
- this._clock = opts.clock ?? systemClock();
250
- this._live = new Live(this, {
251
- console: this._console,
252
- clock: this._clock,
253
- refreshPerSecond: opts.refreshPerSecond ?? 10,
254
- autoRefresh: opts.autoRefresh,
255
- transient: opts.transient,
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._console;
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 [...this._tasks.values()].every(finished);
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._nextId++;
275
- const task = {
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._clock.now(),
282
- finishedTime: undefined,
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 task = this._tasks.get(taskId);
290
- if (!task)
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._clock.now();
300
- recordSample(task.samples, now, completed - task.completed);
301
- task.completed = completed;
302
- if (options.description !== undefined)
303
- task.description = options.description;
304
- if (options.visible !== undefined)
305
- task.visible = options.visible;
306
- if (!finished(task) &&
307
- task.startTime !== undefined &&
308
- task.total !== undefined &&
309
- task.completed >= task.total) {
310
- task.finishedTime = now - task.startTime;
311
- }
312
- if (options.refresh) {
313
- this._live.update(this, { refresh: true });
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 task = this._tasks.get(taskId);
385
+ const held = this._task(taskId);
318
386
  // Rich's `start_task`: a task already running keeps the time it started.
319
- if (task)
320
- task.startTime ??= this._clock.now();
387
+ if (held)
388
+ held.set({ ...held.get(), startTime: held.get().startTime ?? this._host.frameTime });
321
389
  }
322
390
  start() {
323
- this._live.start();
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._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();
327
402
  }
328
403
  refresh() {
329
- 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;
330
411
  }
331
412
  *render(options) {
332
- yield* render(this._table(), options);
413
+ const framed = this._framed(options);
414
+ yield* render(this._table(framed.frame.time), framed);
333
415
  }
334
- // What each column renders for each task is what the table holds, and so is
335
- // all a progress display holds.
336
- *contents() {
337
- yield this._table();
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
- _table() {
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 read of the clock a frame, handed to
346
- // every column as the task's `time` and `elapsed`.
347
- const now = this._clock.now();
348
- 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();
349
443
  if (!state.visible)
350
444
  continue;
351
- const { startTime } = state;
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, 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
@@ -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
- total;
26
- completed;
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.total = options?.total ?? 100;
34
- this.completed = options?.completed ?? 0;
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 === undefined) {
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 caller's; the bar
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), { t: this.pulse.t, key: "progress-bar", theme }), options);
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>;