@promptctl/rich-js 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +7 -0
  2. package/THIRD-PARTY-NOTICES +29 -1
  3. package/dist/core/anchor.d.ts +5 -0
  4. package/dist/core/anchor.js +11 -4
  5. package/dist/core/animation.d.ts +243 -0
  6. package/dist/core/animation.js +375 -0
  7. package/dist/core/ansi.js +2 -2
  8. package/dist/core/box.d.ts +7 -7
  9. package/dist/core/cell-widths.d.ts +9 -0
  10. package/dist/core/cell-widths.js +140 -0
  11. package/dist/core/cells.d.ts +40 -24
  12. package/dist/core/cells.js +151 -66
  13. package/dist/core/clock.d.ts +2 -2
  14. package/dist/core/clock.js +6 -4
  15. package/dist/core/color.d.ts +27 -10
  16. package/dist/core/color.js +21 -29
  17. package/dist/core/console.d.ts +28 -17
  18. package/dist/core/console.js +48 -43
  19. package/dist/core/easing.d.ts +140 -10
  20. package/dist/core/easing.js +328 -50
  21. package/dist/core/emoji.d.ts +1 -1
  22. package/dist/core/emoji.js +1 -2
  23. package/dist/core/export-html.js +2 -1
  24. package/dist/core/export-lines.js +3 -3
  25. package/dist/core/highlighter.d.ts +9 -8
  26. package/dist/core/highlighter.js +3 -2
  27. package/dist/core/json.js +3 -2
  28. package/dist/core/markup.js +16 -12
  29. package/dist/core/measure.js +3 -3
  30. package/dist/core/memo.d.ts +11 -2
  31. package/dist/core/memo.js +16 -0
  32. package/dist/core/oklch.js +17 -6
  33. package/dist/core/osc8.d.ts +1 -1
  34. package/dist/core/osc8.js +4 -5
  35. package/dist/core/paint.d.ts +2 -1
  36. package/dist/core/paint.js +4 -3
  37. package/dist/core/pattern.d.ts +21 -0
  38. package/dist/core/pattern.js +21 -0
  39. package/dist/core/place.js +2 -1
  40. package/dist/core/pretty.js +11 -12
  41. package/dist/core/protocol.d.ts +136 -21
  42. package/dist/core/protocol.js +203 -16
  43. package/dist/core/render.d.ts +15 -2
  44. package/dist/core/render.js +27 -6
  45. package/dist/core/segment.d.ts +31 -6
  46. package/dist/core/segment.js +68 -53
  47. package/dist/core/spinnerData.d.ts +1 -1
  48. package/dist/core/state.d.ts +53 -0
  49. package/dist/core/state.js +82 -0
  50. package/dist/core/strip.d.ts +11 -13
  51. package/dist/core/strip.js +126 -113
  52. package/dist/core/style.d.ts +47 -14
  53. package/dist/core/style.js +74 -34
  54. package/dist/core/text.d.ts +5 -8
  55. package/dist/core/text.js +133 -133
  56. package/dist/core/timeline.d.ts +109 -0
  57. package/dist/core/timeline.js +180 -0
  58. package/dist/core/tween.d.ts +49 -0
  59. package/dist/core/tween.js +71 -0
  60. package/dist/core/view.d.ts +92 -0
  61. package/dist/core/view.js +83 -0
  62. package/dist/host/app.d.ts +49 -21
  63. package/dist/host/app.js +72 -38
  64. package/dist/host/input.js +3 -4
  65. package/dist/index.d.ts +30 -15
  66. package/dist/index.js +20 -6
  67. package/dist/node/traceback.js +4 -3
  68. package/dist/renderables/change.d.ts +42 -0
  69. package/dist/renderables/change.js +112 -0
  70. package/dist/renderables/columns.d.ts +1 -1
  71. package/dist/renderables/columns.js +3 -3
  72. package/dist/renderables/constrain.js +2 -1
  73. package/dist/renderables/effect.d.ts +155 -92
  74. package/dist/renderables/effect.js +203 -136
  75. package/dist/renderables/effects.d.ts +137 -24
  76. package/dist/renderables/effects.js +276 -113
  77. package/dist/renderables/embed.d.ts +1 -1
  78. package/dist/renderables/embed.js +18 -9
  79. package/dist/renderables/flexStrip.js +2 -1
  80. package/dist/renderables/frames.d.ts +45 -43
  81. package/dist/renderables/frames.js +57 -45
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +1 -1
  87. package/dist/renderables/group.js +2 -2
  88. package/dist/renderables/html-entities.d.ts +8 -0
  89. package/dist/renderables/html-entities.js +386 -0
  90. package/dist/renderables/layout.d.ts +10 -9
  91. package/dist/renderables/layout.js +42 -35
  92. package/dist/renderables/lifecycle.d.ts +77 -0
  93. package/dist/renderables/lifecycle.js +132 -0
  94. package/dist/renderables/list-view.d.ts +2 -2
  95. package/dist/renderables/list-view.js +11 -8
  96. package/dist/renderables/live.d.ts +36 -21
  97. package/dist/renderables/live.js +89 -42
  98. package/dist/renderables/markdown-parse.d.ts +109 -0
  99. package/dist/renderables/markdown-parse.js +1427 -0
  100. package/dist/renderables/markdown-url.d.ts +19 -0
  101. package/dist/renderables/markdown-url.js +314 -0
  102. package/dist/renderables/markdown.d.ts +5 -7
  103. package/dist/renderables/markdown.js +356 -683
  104. package/dist/renderables/padding.d.ts +7 -20
  105. package/dist/renderables/padding.js +29 -50
  106. package/dist/renderables/panel.d.ts +30 -32
  107. package/dist/renderables/panel.js +65 -109
  108. package/dist/renderables/progress.d.ts +83 -32
  109. package/dist/renderables/progress.js +186 -90
  110. package/dist/renderables/progressBar.d.ts +22 -12
  111. package/dist/renderables/progressBar.js +33 -10
  112. package/dist/renderables/prompt.js +3 -2
  113. package/dist/renderables/python-space.d.ts +13 -0
  114. package/dist/renderables/python-space.js +25 -0
  115. package/dist/renderables/scroll.d.ts +50 -0
  116. package/dist/renderables/scroll.js +106 -0
  117. package/dist/renderables/spinner.d.ts +57 -24
  118. package/dist/renderables/spinner.js +101 -49
  119. package/dist/renderables/status.js +1 -1
  120. package/dist/renderables/syntax.d.ts +10 -9
  121. package/dist/renderables/syntax.js +5 -3
  122. package/dist/renderables/table.d.ts +48 -46
  123. package/dist/renderables/table.js +170 -208
  124. package/dist/renderables/traceback.d.ts +3 -3
  125. package/dist/renderables/traceback.js +12 -10
  126. package/dist/renderables/tree.d.ts +28 -25
  127. package/dist/renderables/tree.js +108 -58
  128. package/dist/renderables/viewport.d.ts +77 -38
  129. package/dist/renderables/viewport.js +156 -72
  130. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  131. package/dist/template-bindings/animation-funcs.js +132 -14
  132. package/dist/template-bindings/index.d.ts +5 -5
  133. package/dist/template-bindings/index.js +7 -6
  134. package/dist/themes/ramp.d.ts +62 -4
  135. package/dist/themes/ramp.js +82 -4
  136. package/dist/themes/registry.js +5 -7
  137. package/dist/widgets/button.js +3 -2
  138. package/dist/widgets/checkbox.js +4 -3
  139. package/dist/widgets/dropdown.d.ts +3 -1
  140. package/dist/widgets/dropdown.js +20 -15
  141. package/dist/widgets/event-router.js +4 -2
  142. package/dist/widgets/focus-manager.d.ts +23 -14
  143. package/dist/widgets/focus-manager.js +34 -33
  144. package/dist/widgets/hit.d.ts +6 -0
  145. package/dist/widgets/hit.js +8 -1
  146. package/dist/widgets/index.d.ts +1 -1
  147. package/dist/widgets/observable.d.ts +23 -0
  148. package/dist/widgets/observable.js +43 -0
  149. package/dist/widgets/slider.d.ts +1 -1
  150. package/dist/widgets/slider.js +12 -3
  151. package/dist/widgets/static-item.d.ts +1 -1
  152. package/dist/widgets/static-item.js +6 -7
  153. package/dist/widgets/text-input.d.ts +53 -47
  154. package/dist/widgets/text-input.js +456 -399
  155. package/dist/widgets/themed-widget.js +2 -1
  156. package/dist/widgets/toggle.js +4 -3
  157. package/dist/widgets/types.d.ts +10 -4
  158. package/dist/widgets/types.js +8 -2
  159. package/dist/widgets/widget-app.d.ts +32 -6
  160. package/dist/widgets/widget-app.js +74 -27
  161. package/dist/widgets/widget-base.d.ts +2 -2
  162. package/dist/widgets/widget-base.js +29 -21
  163. package/package.json +9 -5
@@ -5,9 +5,10 @@ import { cellCount } from "../core/cells.js";
5
5
  import { ColorDepth, ColorRgba, ColorSpec, resolveTerminal } from "../core/color.js";
6
6
  import { Segment } from "../core/segment.js";
7
7
  import { Style } from "../core/style.js";
8
- import { drawable, getStyle } from "../core/protocol.js";
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* new Effected(track, this.pulseEffect(options, back, depth, theme), { t: this.pulse.t, key: "progress-bar", theme }).render(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
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Whitespace as Python reads it — `str.isspace`, `str.strip`, and `\s` in a
3
+ * `str` pattern of `re`, which are one set. markdown-it-py, the markdown-it
4
+ * Rich runs, reads whitespace through all three, and that set is not
5
+ * JavaScript's: `\s` and `trim` take U+FEFF, which Python does not, and leave
6
+ * U+001C–U+001F and U+0085, which Python takes.
7
+ */
8
+ /** 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
+ export declare const PY_WHITESPACE: string;
10
+ /** Python's `str.strip`, a scan in from each end, so a run of interior whitespace costs one pass, as it does there. */
11
+ export declare function pyStrip(text: string): string;
12
+ /** Python's `str.rstrip`, a scan in from the end — not a `+$` pattern, which retries from every cell of an interior run. */
13
+ export declare function pyRstrip(text: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Whitespace as Python reads it — `str.isspace`, `str.strip`, and `\s` in a
3
+ * `str` pattern of `re`, which are one set. markdown-it-py, the markdown-it
4
+ * Rich runs, reads whitespace through all three, and that set is not
5
+ * JavaScript's: `\s` and `trim` take U+FEFF, which Python does not, and leave
6
+ * U+001C–U+001F and U+0085, which Python takes.
7
+ */
8
+ import { pattern } from "../core/pattern.js";
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`. */
10
+ export const PY_WHITESPACE = String.raw `[\t-\r\x1c-\x20\x85\xa0  - 

   ]`;
11
+ const PY_SPACE = pattern(new RegExp(`^${PY_WHITESPACE}$`));
12
+ /** Python's `str.strip`, a scan in from each end, so a run of interior whitespace costs one pass, as it does there. */
13
+ export function pyStrip(text) {
14
+ let start = 0;
15
+ while (start < text.length && PY_SPACE.test(text[start]))
16
+ start++;
17
+ return pyRstrip(text.slice(start));
18
+ }
19
+ /** Python's `str.rstrip`, a scan in from the end — not a `+$` pattern, which retries from every cell of an interior run. */
20
+ export function pyRstrip(text) {
21
+ let end = text.length;
22
+ while (end > 0 && PY_SPACE.test(text[end - 1]))
23
+ end--;
24
+ return text.slice(0, end);
25
+ }
@@ -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>;
@@ -0,0 +1,106 @@
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 { Measurement } from "../core/measure.js";
38
+ import { render, withBoundedWidth } from "../core/protocol.js";
39
+ import { Segment } from "../core/segment.js";
40
+ const NO_OFFSET = Animation.constant(0);
41
+ /** `content` seen through a slot of `width` by `height` cells, moved in it by `col` and `row` at each moment. */
42
+ export function scroll(content, options) {
43
+ const { width, height, col = NO_OFFSET, row = NO_OFFSET } = options;
44
+ for (const [name, n] of [["width", width], ["height", height]]) {
45
+ if (!(Number.isInteger(n) && n > 0))
46
+ throw new RangeError(`scroll: ${name} must be a positive whole number of cells, got ${n}`);
47
+ }
48
+ return Animation.map(Animation.all([col, row]), ([c, r]) => new ScrollFrame(content, width, height, cells("col", c), cells("row", r)));
49
+ }
50
+ /** `offset` rounded to the nearest cell. [LAW:no-silent-failure] One that is not finite has no cell to move to, and says so rather than drawing a blank slot. */
51
+ function cells(name, offset) {
52
+ if (!Number.isFinite(offset))
53
+ throw new RangeError(`scroll: ${name} must be a finite number of cells, got ${offset}`);
54
+ return Math.round(offset);
55
+ }
56
+ /**
57
+ * The width `content` is drawn at: its own natural one. [LAW:no-silent-failure]
58
+ * Content that takes whatever width it is offered — a `Rule`, a renderable
59
+ * with no `measure` — has none, and is refused by name rather than drawn at
60
+ * an unbounded width it cannot fill.
61
+ */
62
+ function naturalWidth(content, options) {
63
+ const natural = Measurement.get({ ...options, maxWidth: Number.POSITIVE_INFINITY }, content).maximum;
64
+ if (!Number.isFinite(natural)) {
65
+ throw new RangeError("scroll: the content has no natural width — it fills whatever width it is given; give it one with new Constrain(content, width)");
66
+ }
67
+ return natural;
68
+ }
69
+ /** One frame of a scroll: `content`'s cells from column `col` and row `row`, `width` by `height` of them, blank where it has none. */
70
+ class ScrollFrame {
71
+ content;
72
+ width;
73
+ height;
74
+ col;
75
+ row;
76
+ constructor(content, width, height, col, row) {
77
+ this.content = content;
78
+ this.width = width;
79
+ this.height = height;
80
+ this.col = col;
81
+ this.row = row;
82
+ }
83
+ *render(rawOptions) {
84
+ const options = withBoundedWidth(rawOptions, this);
85
+ // The slot is cropped to the room it is given, as a `Panel` of a declared width is.
86
+ const width = Math.min(this.width, options.maxWidth);
87
+ const drawn = [...render(this.content, { ...options, maxWidth: naturalWidth(this.content, options), height: undefined })];
88
+ const lines = Segment.splitLines(drawn);
89
+ const blank = Math.min(width, Math.max(0, -this.col));
90
+ const start = Math.max(0, this.col);
91
+ // [LAW:one-source-of-truth] What the window shows is cut from the whole content, so it may change
92
+ // when anything in the content does — a row above the slot entering pushes every row down, and run
93
+ // backwards, leaving it pulls them up. The cut throws away the cells that hold rode on, so it is
94
+ // told to the frame (`RenderOptions.onHold`).
95
+ options.onHold?.({ lastChange: Segment.lastChange(drawn), nextChange: Segment.nextChange(drawn) });
96
+ for (let r = 0; r < this.height; r++) {
97
+ const line = lines[this.row + r] ?? [];
98
+ const shown = Segment.divide(line, [start, start + width - blank])[1] ?? [];
99
+ yield* Segment.adjustLineLength([new Segment(" ".repeat(blank)), ...shown], width);
100
+ yield Segment.line();
101
+ }
102
+ }
103
+ measure() {
104
+ return { minimum: this.width, maximum: this.width };
105
+ }
106
+ }
@@ -8,7 +8,7 @@
8
8
  * the interval; the beat, the width every frame draws at and which frame `t`
9
9
  * lands on are the frames primitive's, so a spinner is one more sequence on
10
10
  * it and not a second way of turning frames. Custom frames are a `FrameSequence`
11
- * drawn through `Frames` directly. [LAW:one-source-of-truth]
11
+ * placed directly. [LAW:one-source-of-truth]
12
12
  *
13
13
  * It is built once and asked for each frame, as Rich's `Spinner` is built
14
14
  * once and has `render(time)` called on it: the name is parsed and the
@@ -19,41 +19,44 @@
19
19
  * the beat, the width, each frame padded to it — is made there too, so a
20
20
  * drawn frame costs only finding which one `t` lands on.
21
21
  *
22
- * [LAW:no-ambient-temporal-coupling] A Spinner draws nothing on its own:
23
- * only `at(t)` is a renderable, so a spinner cannot be drawn without being
24
- * handed its time, and nothing here reads a clock. Whoever draws frames
25
- * hands each one its time (`core/clock.ts`) — `SpinnerColumn` the task's
26
- * `time`, a `Live` the frame's time on its clock — as Rich's `Spinner.render`
27
- * is handed `console.get_time()` or `task.get_time()`. `t` is the clock's
28
- * time, as `App` and `Task.time` hand it, where Rich's spinner counts from
22
+ * [LAW:no-ambient-temporal-coupling] Placed, a spinner draws at the time of
23
+ * the frame it is drawn in, and nothing here reads a clock. Whoever draws
24
+ * frames hands each one its time (`core/clock.ts`) — a `Progress` the
25
+ * moment its tasks are drawn at, a `Live` the frame's time on its clock — as
26
+ * Rich's `Spinner.render` is handed `console.get_time()` or `task.get_time()`. `t` is the clock's
27
+ * time, as a host writes it on the frame, where Rich's spinner counts from
29
28
  * its first draw: here nothing is kept between frames, so the same `t` draws
30
29
  * the same bytes at one redraw a second or sixty, two spinners on one clock
31
30
  * agree at every `t`, a `Progress` on a hand-moved clock turns its spinners
32
31
  * on that clock, and a draw costs the same at `t = 0` and `t = 10⁶`.
33
32
  *
34
- * `speed` scales the holds, not the time: at speed 2 each frame is held half
35
- * the interval, and a negative speed is the frames in reverse order, each held
36
- * the interval over the speed's size. [LAW:one-source-of-truth] Rich scales
37
- * the time instead, `t × speed` into the beat. At a positive speed the frame
38
- * drawn is the same; but when the frame next changes (`Style.nextChange`) is
39
- * a time on the host's timeline, and a beat over scaled time answers on the
40
- * scaled one — so the sequence is scaled where it is built, and the
41
- * `FrameSequence` stamps host time with nothing to map back. Backwards, this is the plain
42
- * reversed sequence from `t = 0`, where Rich's `int()` truncation holds
43
- * frame 0 for a first interval and then counts down: a departure, made so a
44
- * negative speed is one more sequence on the beat and not a second rule
45
- * about time. A speed of zero would hold a frame forever, which is not a
46
- * beat, and is refused where it is given.
33
+ * `speed` is a `Timeline` (`core/timeline.ts`) playing from `t = 0` at
34
+ * that rate, as Rich scales the time, `t × speed` into the beat: the frames
35
+ * are held for the interval the data gives, and the spinner is asked for the
36
+ * frame at the time the timeline maps the host's to. [LAW:one-source-of-truth]
37
+ * When the frame next changes (`Style.hold`) is a time on the host's
38
+ * timeline, and the sequence answers on its own, so the timeline carries the
39
+ * answer back. `at` hands over the frame alone, as every animation's does;
40
+ * placed, a spinner draws at the frame's time and stamps its cells with that
41
+ * answer for the frames the output draws. Backwards, the frames count down from the last at `t = 0`,
42
+ * a negative time landing in the last frame of the pass before, where Rich's
43
+ * `int()` truncation holds frame 0 for a first interval and then counts
44
+ * down: a departure, made so a negative speed is the one timeline run
45
+ * backwards and not a second rule about time. A speed of zero would hold a
46
+ * frame forever, which a paused timeline is and a rate is not, and the
47
+ * timeline refuses it.
47
48
  */
49
+ import { Animation } from "../core/animation.js";
50
+ import { Segment } from "../core/segment.js";
48
51
  import { Style } from "../core/style.js";
49
52
  import { RichText } from "../core/text.js";
50
- import type { Renderable, Measurable } from "../core/protocol.js";
53
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
51
54
  export interface SpinnerOptions {
52
55
  /** Multiplies the spinner's rate; negative runs it backwards; zero is refused. Defaults to 1. */
53
56
  speed?: number;
54
57
  style?: string | Style;
55
58
  }
56
- export declare class Spinner {
59
+ export declare class Spinner implements Animation<Renderable & Measurable> {
57
60
  readonly name: string;
58
61
  readonly speed: number;
59
62
  readonly style: string | Style;
@@ -64,10 +67,19 @@ export declare class Spinner {
64
67
  * whatever the console says — and a `RichText` changed in place draws as
65
68
  * changed, as the `Text` Rich keeps does. [LAW:single-enforcer]
66
69
  */
67
- text: string | RichText;
70
+ get text(): string | RichText;
71
+ set text(value: string | RichText);
72
+ private readonly _text;
68
73
  private readonly _data;
69
74
  private readonly _named;
70
75
  private readonly _ascii;
76
+ /**
77
+ * The spinner drawn at the frame's time, its cells saying the hold it is in
78
+ * on the host's clock (`stampFrame`) — exactly, for the frames the output
79
+ * draws, where `nextChange` must answer for both. Under reduced motion it
80
+ * is the spinner reduced, which never turns (`shownAt`).
81
+ */
82
+ private readonly _placed;
71
83
  constructor(name?: string, text?: string | RichText, options?: SpinnerOptions);
72
84
  get frames(): readonly string[];
73
85
  /** Milliseconds each frame is held, as the spinner data gives it. */
@@ -77,8 +89,29 @@ export declare class Spinner {
77
89
  * the line wrapped to the width and ended, as the reference's `Text` is —
78
90
  * printed, stacked in a `Group`, or laid out in a `Progress` cell alike.
79
91
  * The frames this output can draw are chosen where the output is known.
92
+ * The frame says nothing of when it turns — `t` may be a time a timeline
93
+ * mapped — so placed, the spinner itself stamps it.
80
94
  */
81
95
  at(t: number): Renderable & Measurable;
96
+ /** The spinner with its motion taken out: its first frame and the label, which never turn, so any time answers. */
97
+ reduced(): Animation<Renderable & Measurable>;
98
+ render(options: RenderOptions): Iterable<Segment>;
99
+ measure(options: RenderOptions): {
100
+ minimum: number;
101
+ maximum: number;
102
+ };
103
+ /** The frame `choose` picks from the frames the output can draw, and the label, saying the hold it is in if `choose` says. */
104
+ private _frame;
105
+ /**
106
+ * When the spinner drawn at `t` next turns, on the host's clock: the
107
+ * sooner of its two sequences' answers, since which one is drawn is the
108
+ * output's to choose and the line spinner that stands in on an ASCII-only
109
+ * output keeps its own interval. An early answer, on the output that draws
110
+ * the other one, which the contract allows; the drawn cells carry the exact one.
111
+ */
112
+ nextChange(t: number): number;
113
+ /** When the spinner drawn at `t` began to be drawn: the later of its two sequences' answers, by the same argument. */
114
+ lastChange(t: number): number;
82
115
  private _render;
83
116
  /**
84
117
  * The frame in progress and the label as one text, as Rich's