@promptctl/rich-js 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/README.md +9 -7
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +145 -0
  7. package/dist/core/box.js +372 -0
  8. package/dist/core/cells.d.ts +192 -0
  9. package/dist/core/cells.js +334 -0
  10. package/dist/core/clock.d.ts +48 -0
  11. package/dist/core/clock.js +42 -0
  12. package/dist/core/color.d.ts +326 -0
  13. package/dist/core/color.js +1225 -0
  14. package/dist/core/console.d.ts +196 -0
  15. package/dist/core/console.js +596 -0
  16. package/dist/core/easing.d.ts +86 -0
  17. package/dist/core/easing.js +171 -0
  18. package/dist/core/emoji.d.ts +24 -0
  19. package/dist/core/emoji.js +391 -0
  20. package/dist/core/env.d.ts +16 -0
  21. package/dist/core/env.js +16 -0
  22. package/dist/core/export-html.d.ts +59 -0
  23. package/dist/core/export-html.js +197 -0
  24. package/dist/core/export-lines.d.ts +130 -0
  25. package/dist/core/export-lines.js +159 -0
  26. package/dist/core/export-svg.d.ts +49 -0
  27. package/dist/core/export-svg.js +213 -0
  28. package/dist/core/fnv1a.d.ts +10 -0
  29. package/dist/core/fnv1a.js +17 -0
  30. package/dist/core/highlighter.d.ts +33 -0
  31. package/dist/core/highlighter.js +190 -0
  32. package/dist/core/json.d.ts +27 -0
  33. package/dist/core/json.js +53 -0
  34. package/dist/core/markup.d.ts +140 -0
  35. package/dist/core/markup.js +654 -0
  36. package/dist/core/measure.d.ts +27 -0
  37. package/dist/core/measure.js +67 -0
  38. package/dist/core/memo.d.ts +19 -0
  39. package/dist/core/memo.js +34 -0
  40. package/dist/core/oklch.d.ts +161 -0
  41. package/dist/core/oklch.js +312 -0
  42. package/dist/core/osc8.d.ts +68 -0
  43. package/dist/core/osc8.js +78 -0
  44. package/dist/core/paint.d.ts +107 -0
  45. package/dist/core/paint.js +150 -0
  46. package/dist/core/place.d.ts +30 -0
  47. package/dist/core/place.js +49 -0
  48. package/dist/core/pretty.d.ts +247 -0
  49. package/dist/core/pretty.js +906 -0
  50. package/dist/core/protocol.d.ts +305 -0
  51. package/dist/core/protocol.js +183 -0
  52. package/dist/core/render.d.ts +87 -0
  53. package/dist/core/render.js +105 -0
  54. package/dist/core/segment.d.ts +162 -0
  55. package/dist/core/segment.js +488 -0
  56. package/dist/core/spinnerData.d.ts +12 -0
  57. package/dist/core/spinnerData.js +117 -0
  58. package/dist/core/strip.d.ts +152 -0
  59. package/dist/core/strip.js +326 -0
  60. package/dist/core/style.d.ts +189 -0
  61. package/dist/core/style.js +758 -0
  62. package/dist/core/subscription.d.ts +12 -0
  63. package/dist/core/subscription.js +12 -0
  64. package/dist/core/text.d.ts +283 -0
  65. package/dist/core/text.js +1097 -0
  66. package/dist/core/wrap.d.ts +46 -0
  67. package/dist/core/wrap.js +140 -0
  68. package/dist/host/app.d.ts +133 -0
  69. package/dist/host/app.js +285 -0
  70. package/dist/host/host-environment.d.ts +26 -0
  71. package/dist/host/host-environment.js +44 -0
  72. package/dist/host/index.d.ts +6 -0
  73. package/dist/host/index.js +3 -0
  74. package/dist/host/terminal-host.d.ts +180 -0
  75. package/dist/host/terminal-host.js +186 -0
  76. package/dist/index.d.ts +98 -0
  77. package/dist/index.js +114 -0
  78. package/dist/node/prompt.d.ts +34 -0
  79. package/dist/node/prompt.js +51 -0
  80. package/dist/node/save.d.ts +18 -0
  81. package/dist/node/save.js +24 -0
  82. package/dist/node/terminal-host.d.ts +114 -0
  83. package/dist/node/terminal-host.js +227 -0
  84. package/dist/node/traceback.d.ts +38 -0
  85. package/dist/node/traceback.js +98 -0
  86. package/dist/renderables/align.d.ts +16 -0
  87. package/dist/renderables/align.js +26 -0
  88. package/dist/renderables/columns.d.ts +77 -0
  89. package/dist/renderables/columns.js +191 -0
  90. package/dist/renderables/constrain.d.ts +15 -0
  91. package/dist/renderables/constrain.js +31 -0
  92. package/dist/renderables/effect.d.ts +98 -0
  93. package/dist/renderables/effect.js +160 -0
  94. package/dist/renderables/embed.d.ts +107 -0
  95. package/dist/renderables/embed.js +160 -0
  96. package/dist/renderables/flexStrip.d.ts +41 -0
  97. package/dist/renderables/flexStrip.js +200 -0
  98. package/dist/renderables/group.d.ts +14 -0
  99. package/dist/renderables/group.js +22 -0
  100. package/dist/renderables/layout.d.ts +75 -0
  101. package/dist/renderables/layout.js +223 -0
  102. package/dist/renderables/live.d.ts +74 -0
  103. package/dist/renderables/live.js +188 -0
  104. package/dist/renderables/markdown.d.ts +38 -0
  105. package/dist/renderables/markdown.js +587 -0
  106. package/dist/renderables/padding.d.ts +59 -0
  107. package/dist/renderables/padding.js +147 -0
  108. package/dist/renderables/panel.d.ts +114 -0
  109. package/dist/renderables/panel.js +303 -0
  110. package/dist/renderables/progress.d.ts +178 -0
  111. package/dist/renderables/progress.js +362 -0
  112. package/dist/renderables/progressBar.d.ts +31 -0
  113. package/dist/renderables/progressBar.js +64 -0
  114. package/dist/renderables/prompt.d.ts +71 -0
  115. package/dist/renderables/prompt.js +165 -0
  116. package/dist/renderables/ratio.d.ts +123 -0
  117. package/dist/renderables/ratio.js +213 -0
  118. package/dist/renderables/rule.d.ts +26 -0
  119. package/dist/renderables/rule.js +86 -0
  120. package/dist/renderables/spinner.d.ts +57 -0
  121. package/dist/renderables/spinner.js +103 -0
  122. package/dist/renderables/status.d.ts +29 -0
  123. package/dist/renderables/status.js +46 -0
  124. package/dist/renderables/syntax.d.ts +65 -0
  125. package/dist/renderables/syntax.js +166 -0
  126. package/dist/renderables/table.d.ts +232 -0
  127. package/dist/renderables/table.js +973 -0
  128. package/dist/renderables/traceback.d.ts +32 -0
  129. package/dist/renderables/traceback.js +279 -0
  130. package/dist/renderables/tree.d.ts +76 -0
  131. package/dist/renderables/tree.js +177 -0
  132. package/dist/renderables/viewport.d.ts +114 -0
  133. package/dist/renderables/viewport.js +196 -0
  134. package/dist/template-bindings/color-funcs.d.ts +109 -0
  135. package/dist/template-bindings/color-funcs.js +243 -0
  136. package/dist/template-bindings/helpers.d.ts +26 -0
  137. package/dist/template-bindings/helpers.js +33 -0
  138. package/dist/template-bindings/index.d.ts +119 -0
  139. package/dist/template-bindings/index.js +162 -0
  140. package/dist/template-bindings/palette-funcs.d.ts +96 -0
  141. package/dist/template-bindings/palette-funcs.js +145 -0
  142. package/dist/template-bindings/style-funcs.d.ts +28 -0
  143. package/dist/template-bindings/style-funcs.js +157 -0
  144. package/dist/themes/buildPalette.d.ts +57 -0
  145. package/dist/themes/buildPalette.js +100 -0
  146. package/dist/themes/colorMath.d.ts +94 -0
  147. package/dist/themes/colorMath.js +280 -0
  148. package/dist/themes/colorRef.d.ts +36 -0
  149. package/dist/themes/colorRef.js +78 -0
  150. package/dist/themes/data/ansi-tables.d.ts +14 -0
  151. package/dist/themes/data/ansi-tables.js +49 -0
  152. package/dist/themes/data/atom-one-dark.d.ts +3 -0
  153. package/dist/themes/data/atom-one-dark.js +177 -0
  154. package/dist/themes/data/atom-one-light.d.ts +3 -0
  155. package/dist/themes/data/atom-one-light.js +177 -0
  156. package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
  157. package/dist/themes/data/catppuccin-frappe.js +177 -0
  158. package/dist/themes/data/catppuccin-latte.d.ts +3 -0
  159. package/dist/themes/data/catppuccin-latte.js +177 -0
  160. package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
  161. package/dist/themes/data/catppuccin-macchiato.js +177 -0
  162. package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
  163. package/dist/themes/data/catppuccin-mocha.js +177 -0
  164. package/dist/themes/data/cyberpunk.d.ts +17 -0
  165. package/dist/themes/data/cyberpunk.js +193 -0
  166. package/dist/themes/data/default.d.ts +3 -0
  167. package/dist/themes/data/default.js +160 -0
  168. package/dist/themes/data/dracula.d.ts +3 -0
  169. package/dist/themes/data/dracula.js +177 -0
  170. package/dist/themes/data/flexoki.d.ts +3 -0
  171. package/dist/themes/data/flexoki.js +177 -0
  172. package/dist/themes/data/gruvbox.d.ts +3 -0
  173. package/dist/themes/data/gruvbox.js +177 -0
  174. package/dist/themes/data/index.d.ts +35 -0
  175. package/dist/themes/data/index.js +64 -0
  176. package/dist/themes/data/monokai.d.ts +3 -0
  177. package/dist/themes/data/monokai.js +177 -0
  178. package/dist/themes/data/nord.d.ts +3 -0
  179. package/dist/themes/data/nord.js +177 -0
  180. package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
  181. package/dist/themes/data/rose-pine-dawn.js +177 -0
  182. package/dist/themes/data/rose-pine-moon.d.ts +3 -0
  183. package/dist/themes/data/rose-pine-moon.js +177 -0
  184. package/dist/themes/data/rose-pine.d.ts +3 -0
  185. package/dist/themes/data/rose-pine.js +177 -0
  186. package/dist/themes/data/solarized-dark.d.ts +3 -0
  187. package/dist/themes/data/solarized-dark.js +177 -0
  188. package/dist/themes/data/solarized-light.d.ts +3 -0
  189. package/dist/themes/data/solarized-light.js +177 -0
  190. package/dist/themes/data/svg-export.d.ts +3 -0
  191. package/dist/themes/data/svg-export.js +177 -0
  192. package/dist/themes/data/textual-ansi.d.ts +3 -0
  193. package/dist/themes/data/textual-ansi.js +160 -0
  194. package/dist/themes/data/textual-dark.d.ts +3 -0
  195. package/dist/themes/data/textual-dark.js +160 -0
  196. package/dist/themes/data/textual-light.d.ts +3 -0
  197. package/dist/themes/data/textual-light.js +160 -0
  198. package/dist/themes/data/tokyo-night.d.ts +3 -0
  199. package/dist/themes/data/tokyo-night.js +177 -0
  200. package/dist/themes/data/types.d.ts +31 -0
  201. package/dist/themes/data/types.js +10 -0
  202. package/dist/themes/palette.d.ts +45 -0
  203. package/dist/themes/palette.js +71 -0
  204. package/dist/themes/ramp.d.ts +70 -0
  205. package/dist/themes/ramp.js +103 -0
  206. package/dist/themes/registry.d.ts +44 -0
  207. package/dist/themes/registry.js +105 -0
  208. package/dist/themes/terminalThemes.d.ts +49 -0
  209. package/dist/themes/terminalThemes.js +63 -0
  210. package/dist/themes/transpose.d.ts +87 -0
  211. package/dist/themes/transpose.js +138 -0
  212. package/dist/widgets/button.d.ts +45 -0
  213. package/dist/widgets/button.js +177 -0
  214. package/dist/widgets/checkbox.d.ts +38 -0
  215. package/dist/widgets/checkbox.js +135 -0
  216. package/dist/widgets/dropdown.d.ts +64 -0
  217. package/dist/widgets/dropdown.js +416 -0
  218. package/dist/widgets/event-router.d.ts +71 -0
  219. package/dist/widgets/event-router.js +629 -0
  220. package/dist/widgets/focus-manager.d.ts +45 -0
  221. package/dist/widgets/focus-manager.js +191 -0
  222. package/dist/widgets/hit.d.ts +64 -0
  223. package/dist/widgets/hit.js +113 -0
  224. package/dist/widgets/index.d.ts +20 -0
  225. package/dist/widgets/index.js +14 -0
  226. package/dist/widgets/ink.d.ts +22 -0
  227. package/dist/widgets/ink.js +31 -0
  228. package/dist/widgets/slider.d.ts +54 -0
  229. package/dist/widgets/slider.js +247 -0
  230. package/dist/widgets/static-item.d.ts +34 -0
  231. package/dist/widgets/static-item.js +54 -0
  232. package/dist/widgets/text-input.d.ts +283 -0
  233. package/dist/widgets/text-input.js +1174 -0
  234. package/dist/widgets/themed-widget.d.ts +20 -0
  235. package/dist/widgets/themed-widget.js +89 -0
  236. package/dist/widgets/toggle.d.ts +41 -0
  237. package/dist/widgets/toggle.js +157 -0
  238. package/dist/widgets/types.d.ts +87 -0
  239. package/dist/widgets/types.js +30 -0
  240. package/dist/widgets/widget-app.d.ts +65 -0
  241. package/dist/widgets/widget-app.js +206 -0
  242. package/dist/widgets/widget-base.d.ts +42 -0
  243. package/dist/widgets/widget-base.js +172 -0
  244. package/package.json +16 -2
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Progress — displays continuously updated progress bars.
3
+ */
4
+ import type { Segment } from "../core/segment.js";
5
+ import { RichText } from "../core/text.js";
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";
11
+ import type { Renderable, RenderOptions } from "../core/protocol.js";
12
+ export interface TaskOptions {
13
+ total?: number;
14
+ start?: boolean;
15
+ visible?: boolean;
16
+ }
17
+ export interface TaskUpdateOptions {
18
+ completed?: number;
19
+ advance?: number;
20
+ description?: string;
21
+ visible?: boolean;
22
+ refresh?: boolean;
23
+ }
24
+ 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;
32
+ /**
33
+ * Rich's `Task.elapsed`: seconds since the task started, as of the frame
34
+ * being drawn — `Progress` reads its clock once a frame and hands columns
35
+ * the result, so no column reads a clock. Undefined until the task starts.
36
+ */
37
+ elapsed: number | undefined;
38
+ /**
39
+ * Rich's `Task.finished_time`: the elapsed time the first time an update
40
+ * finds a started task at its total, kept from then on, so a finished task
41
+ * stays finished however its count moves after.
42
+ */
43
+ finishedTime: number | undefined;
44
+ /**
45
+ * Rich's `Task.speed`: steps a second over the updates that moved the task
46
+ * in the `SPEED_ESTIMATE_PERIOD` before its latest update — measured back from
47
+ * that update, not from now, so a task that stops being updated keeps the
48
+ * speed it had. Undefined until the task has started and two such updates lie
49
+ * apart in time.
50
+ */
51
+ speed: number | undefined;
52
+ }
53
+ /**
54
+ * One cell of a `Progress` row, made for the task on that row. Not a
55
+ * `Renderable`: a column has nothing to draw without a task, as the
56
+ * reference's `ProgressColumn` is a callable of one. What it returns is the
57
+ * cell's content, which the progress grid lays out like any table cell's —
58
+ * width, wrapping and justify are decided once, in the cell, as Rich's
59
+ * `make_tasks_table` decides them. [LAW:single-enforcer]
60
+ */
61
+ export interface ProgressColumn {
62
+ /**
63
+ * The grid column this one is laid out in, Rich's `get_table_column()`:
64
+ * whether its cell may wrap, how it justifies, its share of the width.
65
+ */
66
+ readonly tableColumn: ColumnOptions;
67
+ render(task: Task): Renderable;
68
+ }
69
+ export declare class TextColumn implements ProgressColumn {
70
+ readonly format: string;
71
+ /** Text is cut, never wrapped, as Rich's `TextColumn` defaults to `Column(no_wrap=True)`. */
72
+ readonly tableColumn: ColumnOptions;
73
+ constructor(format?: string);
74
+ render(task: Task): RichText;
75
+ }
76
+ export declare class BarColumn implements ProgressColumn {
77
+ readonly barWidth: number;
78
+ readonly tableColumn: ColumnOptions;
79
+ constructor(barWidth?: number);
80
+ render(task: Task): ProgressBar;
81
+ }
82
+ /**
83
+ * Rich's `{task.percentage:>3.0f}%`: the percentage right-aligned in three
84
+ * cells, so the column is four wide from 0% to 100% and the row does not
85
+ * shift as it counts. A task with no total shows nothing, Rich's
86
+ * `text_format_no_percentage`.
87
+ */
88
+ export declare class TaskProgressColumn implements ProgressColumn {
89
+ /** A `TextColumn` in Rich, and laid out as one. */
90
+ readonly tableColumn: ColumnOptions;
91
+ render(task: Task): RichText;
92
+ }
93
+ export interface TimeRemainingColumnOptions {
94
+ /**
95
+ * Once the task is finished, draw the time it took, styled
96
+ * `progress.elapsed`, in place of the zero left to go: Rich's
97
+ * `elapsed_when_finished`. Defaults to false.
98
+ */
99
+ elapsedWhenFinished?: boolean;
100
+ }
101
+ export declare class TimeRemainingColumn implements ProgressColumn {
102
+ readonly tableColumn: ColumnOptions;
103
+ readonly elapsedWhenFinished: boolean;
104
+ constructor(options?: TimeRemainingColumnOptions);
105
+ render(task: Task): RichText;
106
+ }
107
+ export declare class TimeElapsedColumn implements ProgressColumn {
108
+ readonly tableColumn: ColumnOptions;
109
+ render(task: Task): RichText;
110
+ }
111
+ export interface SpinnerColumnOptions {
112
+ /** The frame's style. Defaults to `progress.spinner`, a name its console's theme resolves. */
113
+ style?: string | Style;
114
+ /** Multiplies the spinner's frame rate. Defaults to 1. */
115
+ speed?: number;
116
+ /** Drawn in place of the frame once the task reaches its total; a string is markup. Defaults to `" "`. */
117
+ finishedText?: string | RichText;
118
+ }
119
+ export declare class SpinnerColumn implements ProgressColumn {
120
+ readonly tableColumn: ColumnOptions;
121
+ private _spinner;
122
+ private _finishedText;
123
+ constructor(spinnerName?: string, options?: SpinnerColumnOptions);
124
+ render(task: Task): Renderable;
125
+ }
126
+ export interface MofNCompleteColumnOptions {
127
+ /** Drawn between the two counts: Rich's `separator`. Defaults to `"/"`. */
128
+ separator?: string;
129
+ }
130
+ /**
131
+ * Rich's `MofNCompleteColumn`: both counts as whole numbers, the completed one
132
+ * padded to the total's width so the column holds still as it counts up to its
133
+ * total, styled `progress.download`.
134
+ */
135
+ export declare class MofNCompleteColumn implements ProgressColumn {
136
+ readonly tableColumn: ColumnOptions;
137
+ readonly separator: string;
138
+ constructor(options?: MofNCompleteColumnOptions);
139
+ render(task: Task): RichText;
140
+ }
141
+ export interface ProgressOptions {
142
+ refreshPerSecond?: number;
143
+ autoRefresh?: boolean;
144
+ transient?: boolean;
145
+ expand?: boolean;
146
+ console?: Console;
147
+ /**
148
+ * What tasks are timed by and frames are drawn on, Rich's `get_time`;
149
+ * defaults to the platform's monotonic clock. Handed on to the `Live`.
150
+ */
151
+ clock?: Clock;
152
+ }
153
+ export declare class Progress implements Renderable {
154
+ private _columns;
155
+ private _tasks;
156
+ private _nextId;
157
+ private _live;
158
+ private _console;
159
+ private readonly _clock;
160
+ readonly expand: boolean;
161
+ constructor(...columns: (ProgressColumn | ProgressOptions)[]);
162
+ get console(): Console;
163
+ /** Rich's `Progress.finished`: every task finished, and so true with none. */
164
+ get finished(): boolean;
165
+ static getDefaultColumns(): ProgressColumn[];
166
+ addTask(description: string, options?: TaskOptions): number;
167
+ updateTask(taskId: number, options: TaskUpdateOptions): void;
168
+ startTask(taskId: number): void;
169
+ start(): void;
170
+ stop(): void;
171
+ refresh(): void;
172
+ render(options: RenderOptions): Iterable<Segment>;
173
+ }
174
+ export declare function track<T>(iterable: Iterable<T>, options?: {
175
+ description?: string;
176
+ total?: number;
177
+ console?: Console;
178
+ }): Iterable<T>;
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Progress — displays continuously updated progress bars.
3
+ */
4
+ import { RichText } from "../core/text.js";
5
+ import { Console } from "../core/console.js";
6
+ import { escape as escapeMarkup, readStr, renderStr } from "../core/markup.js";
7
+ import { ProgressBar } from "./progressBar.js";
8
+ import { Spinner } from "./spinner.js";
9
+ import { Live } from "./live.js";
10
+ import { systemClock } from "../core/clock.js";
11
+ import { Table } from "./table.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
+ }
58
+ export class TextColumn {
59
+ format;
60
+ /** Text is cut, never wrapped, as Rich's `TextColumn` defaults to `Column(no_wrap=True)`. */
61
+ tableColumn = { noWrap: true };
62
+ constructor(format) {
63
+ this.format = format ?? "{task.description}";
64
+ }
65
+ render(task) {
66
+ // [LAW:single-enforcer] Format strings flow through the markup parser so
67
+ // tags like `[progress.description]` become Style spans, not literal text.
68
+ // Task descriptions are escaped first to prevent injection of stray tags.
69
+ const description = escapeMarkup(task.description);
70
+ // Use a callback so `$&`/`$1`/`$$` in the task description aren't
71
+ // reinterpreted by String.replace as replacement patterns.
72
+ const formatted = this.format.replace(/\{task\.description\}/g, () => description);
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 });
77
+ }
78
+ }
79
+ export class BarColumn {
80
+ barWidth;
81
+ tableColumn = {};
82
+ constructor(barWidth) {
83
+ this.barWidth = barWidth ?? 40;
84
+ }
85
+ render(task) {
86
+ return new ProgressBar({
87
+ total: task.total ?? 100,
88
+ completed: task.completed,
89
+ width: this.barWidth,
90
+ });
91
+ }
92
+ }
93
+ /**
94
+ * Rich's `{task.percentage:>3.0f}%`: the percentage right-aligned in three
95
+ * cells, so the column is four wide from 0% to 100% and the row does not
96
+ * shift as it counts. A task with no total shows nothing, Rich's
97
+ * `text_format_no_percentage`.
98
+ */
99
+ export class TaskProgressColumn {
100
+ /** A `TextColumn` in Rich, and laid out as one. */
101
+ tableColumn = { noWrap: true };
102
+ render(task) {
103
+ const text = task.total === undefined ? "" : `${String(roundHalfEven(percentage(task))).padStart(3)}%`;
104
+ return new RichText(text, { style: "progress.percentage" });
105
+ }
106
+ }
107
+ /** Rich's `Task.percentage`: 0 without a total, and clamped to [0, 100]. */
108
+ function percentage(task) {
109
+ return task.total ? Math.min(100, Math.max(0, (task.completed / task.total) * 100)) : 0;
110
+ }
111
+ /**
112
+ * Python's `format(x, ".0f")`: the nearest integer to the double's exact
113
+ * value, a tie going to the even one. `Math.round` is exact too but sends a
114
+ * tie up, so only a tie is corrected.
115
+ */
116
+ function roundHalfEven(x) {
117
+ const up = Math.round(x);
118
+ return up - x === 0.5 && up % 2 !== 0 ? up - 1 : up;
119
+ }
120
+ export class TimeRemainingColumn {
121
+ tableColumn = {};
122
+ elapsedWhenFinished;
123
+ constructor(options) {
124
+ this.elapsedWhenFinished = options?.elapsedWhenFinished ?? false;
125
+ }
126
+ render(task) {
127
+ const took = this.elapsedWhenFinished && finished(task) ? task.finishedTime : undefined;
128
+ const time = took ?? timeRemaining(task);
129
+ const style = took === undefined ? "progress.remaining" : "progress.elapsed";
130
+ // A task with no total has no time to estimate, and Rich draws its cell empty.
131
+ return new RichText(task.total === undefined ? "" : clockFace(time), { style });
132
+ }
133
+ }
134
+ /**
135
+ * Rich's `Task.time_remaining`: nothing left once the task is finished,
136
+ * however long ago that was, unknown until it has a speed against a total,
137
+ * and otherwise whole seconds, rounded up.
138
+ */
139
+ function timeRemaining(task) {
140
+ if (finished(task))
141
+ return 0;
142
+ if (!task.speed || task.total === undefined)
143
+ return undefined;
144
+ return Math.ceil((task.total - task.completed) / task.speed);
145
+ }
146
+ export class TimeElapsedColumn {
147
+ tableColumn = {};
148
+ render(task) {
149
+ // Rich's `finished_time if finished else elapsed`: a finished task's clock
150
+ // holds where it stopped, and one never started has no time to show.
151
+ const time = finished(task) ? task.finishedTime : task.elapsed;
152
+ return new RichText(clockFace(time), { style: "progress.elapsed" });
153
+ }
154
+ }
155
+ /** A time as the time columns draw it, `-:--:--` when there is none to draw. */
156
+ function clockFace(seconds) {
157
+ return seconds === undefined ? "-:--:--" : formatTime(seconds);
158
+ }
159
+ export class SpinnerColumn {
160
+ tableColumn = {};
161
+ _spinner;
162
+ _finishedText;
163
+ constructor(spinnerName, options) {
164
+ this._spinner = new Spinner(spinnerName, "", {
165
+ style: options?.style ?? "progress.spinner",
166
+ speed: options?.speed,
167
+ });
168
+ // Markup whatever the console says, as Rich's `Text.from_markup`. A
169
+ // `RichText` is copied, since the caller still holds it.
170
+ const finishedText = options?.finishedText ?? " ";
171
+ this._finishedText = typeof finishedText === "string" ? readStr(finishedText, true) : finishedText.copy();
172
+ }
173
+ render(task) {
174
+ return finished(task) ? this._finishedText : this._spinner;
175
+ }
176
+ }
177
+ /**
178
+ * Rich's `MofNCompleteColumn`: both counts as whole numbers, the completed one
179
+ * padded to the total's width so the column holds still as it counts up to its
180
+ * total, styled `progress.download`.
181
+ */
182
+ export class MofNCompleteColumn {
183
+ tableColumn = {};
184
+ separator;
185
+ constructor(options) {
186
+ this.separator = options?.separator ?? "/";
187
+ }
188
+ render(task) {
189
+ const total = task.total === undefined ? "?" : wholeNumber(task.total);
190
+ const completed = wholeNumber(task.completed).padStart(total.length);
191
+ return new RichText(`${completed}${this.separator}${total}`, { style: "progress.download" });
192
+ }
193
+ }
194
+ /**
195
+ * Python's `str(int(x))`: truncated toward zero and written out in full,
196
+ * never in exponent form. `Progress` admits only finite counts, so `BigInt`
197
+ * has nothing here to refuse.
198
+ */
199
+ function wholeNumber(x) {
200
+ return BigInt(Math.trunc(x)).toString();
201
+ }
202
+ /**
203
+ * Rich's clock face: `int()` the seconds, then Python's floor `divmod` by 60
204
+ * twice, so a negative estimate — a task past its total — reads as Rich's
205
+ * does, `-3` as `-1:59:57`.
206
+ */
207
+ function formatTime(seconds) {
208
+ const whole = Math.trunc(seconds);
209
+ const minutes = Math.floor(whole / 60);
210
+ const h = Math.floor(minutes / 60);
211
+ const m = minutes - h * 60;
212
+ const s = whole - minutes * 60;
213
+ return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
214
+ }
215
+ export class Progress {
216
+ _columns;
217
+ _tasks;
218
+ _nextId;
219
+ _live;
220
+ _console;
221
+ _clock;
222
+ expand;
223
+ constructor(...columns) {
224
+ // Last arg might be options
225
+ let opts = {};
226
+ const cols = [];
227
+ for (const arg of columns) {
228
+ if ("render" in arg) {
229
+ cols.push(arg);
230
+ }
231
+ else {
232
+ opts = arg;
233
+ }
234
+ }
235
+ if (cols.length === 0) {
236
+ cols.push(new TextColumn("[progress.description]{task.description}"), new BarColumn(), new TaskProgressColumn(), new TimeRemainingColumn());
237
+ }
238
+ this._columns = cols;
239
+ this._tasks = new Map();
240
+ this._nextId = 1;
241
+ this._console = opts.console ?? new Console();
242
+ this.expand = opts.expand ?? false;
243
+ this._clock = opts.clock ?? systemClock();
244
+ this._live = new Live(this, {
245
+ console: this._console,
246
+ clock: this._clock,
247
+ refreshPerSecond: opts.refreshPerSecond ?? 10,
248
+ autoRefresh: opts.autoRefresh,
249
+ transient: opts.transient,
250
+ });
251
+ }
252
+ get console() {
253
+ return this._console;
254
+ }
255
+ /** Rich's `Progress.finished`: every task finished, and so true with none. */
256
+ get finished() {
257
+ return [...this._tasks.values()].every(finished);
258
+ }
259
+ static getDefaultColumns() {
260
+ return [
261
+ new TextColumn("[progress.description]{task.description}"),
262
+ new BarColumn(),
263
+ new TaskProgressColumn(),
264
+ new TimeRemainingColumn(),
265
+ ];
266
+ }
267
+ addTask(description, options) {
268
+ const id = this._nextId++;
269
+ const task = {
270
+ id,
271
+ description,
272
+ total: options?.total === undefined ? undefined : finiteCount("total", options.total),
273
+ completed: 0,
274
+ visible: options?.visible !== false,
275
+ startTime: options?.start === false ? undefined : this._clock.now(),
276
+ finishedTime: undefined,
277
+ samples: [],
278
+ };
279
+ this._tasks.set(id, task);
280
+ return id;
281
+ }
282
+ updateTask(taskId, options) {
283
+ const task = this._tasks.get(taskId);
284
+ if (!task)
285
+ return;
286
+ // Rich's order, `completed` then `advance`, checked whole before any field
287
+ // is written, so a refused update leaves the task as it was.
288
+ const advance = finiteCount("advance", options.advance ?? 0);
289
+ const completed = finiteCount("completed", (options.completed ?? task.completed) + advance);
290
+ const now = this._clock.now();
291
+ recordSample(task.samples, now, completed - task.completed);
292
+ task.completed = completed;
293
+ if (options.description !== undefined)
294
+ task.description = options.description;
295
+ if (options.visible !== undefined)
296
+ task.visible = options.visible;
297
+ if (!finished(task) &&
298
+ task.startTime !== undefined &&
299
+ task.total !== undefined &&
300
+ task.completed >= task.total) {
301
+ task.finishedTime = now - task.startTime;
302
+ }
303
+ if (options.refresh) {
304
+ this._live.update(this, { refresh: true });
305
+ }
306
+ }
307
+ startTask(taskId) {
308
+ const task = this._tasks.get(taskId);
309
+ // Rich's `start_task`: a task already running keeps the time it started.
310
+ if (task)
311
+ task.startTime ??= this._clock.now();
312
+ }
313
+ start() {
314
+ this._live.start();
315
+ }
316
+ stop() {
317
+ this._live.stop();
318
+ }
319
+ refresh() {
320
+ this._live.refresh();
321
+ }
322
+ *render(options) {
323
+ // Rich's `make_tasks_table`: each column laid out as it says.
324
+ const table = Table.grid({ expand: this.expand });
325
+ for (const col of this._columns) {
326
+ table.addColumn(undefined, col.tableColumn);
327
+ }
328
+ // [LAW:effects-at-boundaries] One read of the clock a frame, handed to
329
+ // every column as the task's `elapsed`.
330
+ const now = this._clock.now();
331
+ for (const { samples, ...state } of this._tasks.values()) {
332
+ if (!state.visible)
333
+ continue;
334
+ const { startTime } = state;
335
+ const started = startTime !== undefined;
336
+ const task = {
337
+ ...state,
338
+ elapsed: started ? now - startTime : undefined,
339
+ speed: started ? sampleSpeed(samples) : undefined,
340
+ };
341
+ table.addRow(...this._columns.map((col) => col.render(task)));
342
+ }
343
+ yield* table.render(options);
344
+ }
345
+ }
346
+ // --- track ---
347
+ export function* track(iterable, options) {
348
+ const items = [...iterable];
349
+ const total = options?.total ?? items.length;
350
+ const progress = new Progress({ console: options?.console });
351
+ const taskId = progress.addTask(options?.description ?? "Working...", { total });
352
+ progress.start();
353
+ try {
354
+ for (let i = 0; i < items.length; i++) {
355
+ yield items[i];
356
+ progress.updateTask(taskId, { completed: i + 1 });
357
+ }
358
+ }
359
+ finally {
360
+ progress.stop();
361
+ }
362
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * ProgressBar — a visual progress bar rendered with block characters.
3
+ */
4
+ import { Segment } from "../core/segment.js";
5
+ import { Style } from "../core/style.js";
6
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ export interface ProgressBarOptions {
8
+ total?: number;
9
+ completed?: number;
10
+ width?: number;
11
+ pulse?: boolean;
12
+ style?: string | Style;
13
+ completeStyle?: string | Style;
14
+ finishedStyle?: string | Style;
15
+ }
16
+ export declare class ProgressBar implements Renderable, Measurable {
17
+ total: number;
18
+ completed: number;
19
+ readonly width: number | undefined;
20
+ readonly pulse: boolean;
21
+ readonly style: string | Style;
22
+ readonly completeStyle: string | Style;
23
+ readonly finishedStyle: string | Style;
24
+ constructor(options?: ProgressBarOptions);
25
+ render(options: RenderOptions): Iterable<Segment>;
26
+ /** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
27
+ measure(options: RenderOptions): {
28
+ minimum: number;
29
+ maximum: number;
30
+ };
31
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ProgressBar — a visual progress bar rendered with block characters.
3
+ */
4
+ import { cellCount } from "../core/cells.js";
5
+ import { Segment } from "../core/segment.js";
6
+ import { NULL_STYLE } from "../core/style.js";
7
+ import { drawable, getStyle } from "../core/protocol.js";
8
+ const BAR = "━";
9
+ const ASCII_BAR = "-";
10
+ const HALF_BAR_RIGHT = "╸";
11
+ const HALF_BAR_LEFT = "╺";
12
+ export class ProgressBar {
13
+ total;
14
+ completed;
15
+ width;
16
+ pulse;
17
+ style;
18
+ completeStyle;
19
+ finishedStyle;
20
+ constructor(options) {
21
+ this.total = options?.total ?? 100;
22
+ this.completed = options?.completed ?? 0;
23
+ this.width = options?.width;
24
+ this.pulse = options?.pulse ?? false;
25
+ this.style = options?.style ?? NULL_STYLE;
26
+ this.completeStyle = options?.completeStyle ?? "bar.complete";
27
+ this.finishedStyle = options?.finishedStyle ?? "bar.finished";
28
+ }
29
+ *render(options) {
30
+ // Never wider than offered, as Rich's `min(self.width or max_width, max_width)`.
31
+ // [LAW:parse-dont-validate] parsed as a cell count, so a negative width draws nothing, as Python's `"━" * -2` is "".
32
+ const width = cellCount(Math.min(this.width || options.maxWidth, options.maxWidth));
33
+ // Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and
34
+ // a total of zero is a bar already full.
35
+ const completed = Math.min(this.total, Math.max(0, this.completed));
36
+ const completeHalves = this.total ? Math.trunc((width * 2 * completed) / this.total) : width * 2;
37
+ const barCount = Math.floor(completeHalves / 2);
38
+ const halfBarCount = completeHalves % 2;
39
+ const isFinished = this.completed >= this.total;
40
+ const fill = getStyle(options, isFinished ? this.finishedStyle : this.completeStyle);
41
+ const back = getStyle(options, this.style);
42
+ const fillStyle = fill.isNull ? undefined : fill;
43
+ const backStyle = back.isNull ? undefined : back;
44
+ const bar = drawable(options, BAR, ASCII_BAR);
45
+ if (barCount)
46
+ yield new Segment(bar.repeat(barCount), fillStyle);
47
+ if (halfBarCount)
48
+ yield new Segment(drawable(options, HALF_BAR_RIGHT, " "), fillStyle);
49
+ // The back is drawn in colour alone, so an output with none leaves it blank.
50
+ if (options.colorSystem === null)
51
+ return;
52
+ let remaining = width - barCount - halfBarCount;
53
+ if (remaining && !halfBarCount && barCount) {
54
+ yield new Segment(drawable(options, HALF_BAR_LEFT, " "), backStyle);
55
+ remaining -= 1;
56
+ }
57
+ if (remaining)
58
+ yield new Segment(bar.repeat(remaining), backStyle);
59
+ }
60
+ /** A width given is the bar's exact width, as Rich measures it; one left open fills what it is offered, down to 4. */
61
+ measure(options) {
62
+ return this.width === undefined ? { minimum: 4, maximum: options.maxWidth } : { minimum: this.width, maximum: this.width };
63
+ }
64
+ }