@promptctl/rich-js 0.18.1 → 0.20.0

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