@promptctl/rich-js 0.25.2 → 0.27.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 (49) hide show
  1. package/dist/core/animation.d.ts +26 -8
  2. package/dist/core/animation.js +32 -11
  3. package/dist/core/color.d.ts +20 -11
  4. package/dist/core/color.js +26 -21
  5. package/dist/core/console.d.ts +8 -0
  6. package/dist/core/console.js +2 -1
  7. package/dist/core/easing.js +6 -1
  8. package/dist/core/oklch.d.ts +2 -0
  9. package/dist/core/oklch.js +3 -3
  10. package/dist/core/protocol.d.ts +26 -16
  11. package/dist/core/render.d.ts +1 -1
  12. package/dist/core/render.js +1 -0
  13. package/dist/core/view.d.ts +22 -8
  14. package/dist/core/view.js +32 -8
  15. package/dist/host/app.d.ts +4 -4
  16. package/dist/host/app.js +5 -5
  17. package/dist/index.d.ts +6 -5
  18. package/dist/renderables/effect.d.ts +44 -19
  19. package/dist/renderables/effect.js +20 -13
  20. package/dist/renderables/effects.d.ts +15 -9
  21. package/dist/renderables/effects.js +166 -34
  22. package/dist/renderables/lifecycle.d.ts +38 -28
  23. package/dist/renderables/lifecycle.js +85 -45
  24. package/dist/renderables/live.d.ts +3 -3
  25. package/dist/renderables/live.js +4 -4
  26. package/dist/renderables/progress.d.ts +11 -4
  27. package/dist/renderables/progress.js +29 -17
  28. package/dist/themes/buildPalette.d.ts +14 -39
  29. package/dist/themes/buildPalette.js +196 -67
  30. package/dist/themes/colorMath.d.ts +10 -4
  31. package/dist/themes/colorMath.js +33 -1
  32. package/dist/themes/data/types.d.ts +3 -1
  33. package/dist/themes/palette.d.ts +22 -6
  34. package/dist/themes/palette.js +13 -21
  35. package/dist/themes/registry.d.ts +2 -3
  36. package/dist/themes/registry.js +7 -26
  37. package/dist/themes/terminalThemes.d.ts +6 -5
  38. package/dist/themes/terminalThemes.js +6 -5
  39. package/dist/themes/transpose.d.ts +5 -10
  40. package/dist/themes/transpose.js +13 -24
  41. package/dist/widgets/event-router.js +2 -2
  42. package/dist/widgets/ink.d.ts +5 -4
  43. package/dist/widgets/ink.js +6 -9
  44. package/dist/widgets/text-input.d.ts +1 -1
  45. package/dist/widgets/text-input.js +41 -32
  46. package/dist/widgets/types.d.ts +2 -2
  47. package/dist/widgets/widget-app.d.ts +3 -3
  48. package/dist/widgets/widget-app.js +5 -5
  49. package/package.json +1 -1
@@ -137,14 +137,23 @@ export declare function stampFrame(segments: Iterable<Segment>, hold: Hold): Ite
137
137
  */
138
138
  export declare function frameTimeOf(options: RenderOptions): number;
139
139
  /**
140
- * An animation's face, `render`: drawn at the time of the frame it is drawn
141
- * in (`frameTimeOf`), the value `shownAt` picks, every segment stamped with
142
- * its hold there under whatever its own cells say, and the hold told to the
143
- * frame (`RenderOptions.onHold`), because a frame of no cells — an element
144
- * not yet entered — may have cells later, and must say when without taking a
145
- * row to say it in. A container — the library's or a user's — that draws its
146
- * children through `render` draws an animation among them with no word about
147
- * time: the hold goes back the way the time came in. [LAW:composability]
140
+ * An animation placed in the frame `options` draws: its value at the frame's
141
+ * time (`frameTimeOf`, `shownAt`), drawn by `draw`, every segment stamped
142
+ * with its hold there under whatever its own cells say, and the hold told to
143
+ * the frame (`RenderOptions.onHold`), because a frame of no cells — an
144
+ * element not yet entered — may have cells later, and must say when without
145
+ * taking a row to say it in. [LAW:single-enforcer] The one placement: an
146
+ * animation of renderables (`placedRender`) and a widget drawing one cell
147
+ * from an animation of its own (a caret's blink) place through it, so how a
148
+ * time reaches an animation and how its hold reaches the frame is written
149
+ * once.
150
+ */
151
+ export declare function placed<T>(animation: Timed<T>, options: RenderOptions, draw: (value: T) => Iterable<Segment>): Iterable<Segment>;
152
+ /**
153
+ * An animation's face, `render`: `placed`, its value drawn as the renderable
154
+ * it is. A container — the library's or a user's — that draws its children
155
+ * through `render` draws an animation among them with no word about time:
156
+ * the hold goes back the way the time came in. [LAW:composability]
148
157
  */
149
158
  export declare function placedRender(animation: Timed<unknown>, options: RenderOptions): Iterable<Segment>;
150
159
  /**
@@ -169,6 +178,15 @@ export declare const Animation: {
169
178
  * renderables throws when it is drawn.
170
179
  */
171
180
  of<X extends Timed<unknown>>(timed: X): Faced<NoInfer<X>>;
181
+ /**
182
+ * Whether `animation` never changes again from `t`: a one-shot that has
183
+ * run, a loop at a depth that shows no motion. [LAW:single-enforcer] The
184
+ * one reading of "never": a `Transition`'s `done` and a lifecycle's
185
+ * arrived and gone moments are this, so what ends a one-shot is written
186
+ * once. An early answer the contract allows is a finite one, so `Infinity`
187
+ * is only ever a true never.
188
+ */
189
+ done(animation: Timed<unknown>, t: number): boolean;
172
190
  /** `value` at every time, never changing. */
173
191
  constant<T>(value: T): Animation<T>;
174
192
  /**
@@ -126,22 +126,32 @@ function drawable(value) {
126
126
  return value;
127
127
  }
128
128
  /**
129
- * An animation's face, `render`: drawn at the time of the frame it is drawn
130
- * in (`frameTimeOf`), the value `shownAt` picks, every segment stamped with
131
- * its hold there under whatever its own cells say, and the hold told to the
132
- * frame (`RenderOptions.onHold`), because a frame of no cells — an element
133
- * not yet entered — may have cells later, and must say when without taking a
134
- * row to say it in. A container — the library's or a user's — that draws its
135
- * children through `render` draws an animation among them with no word about
136
- * time: the hold goes back the way the time came in. [LAW:composability]
129
+ * An animation placed in the frame `options` draws: its value at the frame's
130
+ * time (`frameTimeOf`, `shownAt`), drawn by `draw`, every segment stamped
131
+ * with its hold there under whatever its own cells say, and the hold told to
132
+ * the frame (`RenderOptions.onHold`), because a frame of no cells — an
133
+ * element not yet entered — may have cells later, and must say when without
134
+ * taking a row to say it in. [LAW:single-enforcer] The one placement: an
135
+ * animation of renderables (`placedRender`) and a widget drawing one cell
136
+ * from an animation of its own (a caret's blink) place through it, so how a
137
+ * time reaches an animation and how its hold reaches the frame is written
138
+ * once.
137
139
  */
138
- export function* placedRender(animation, options) {
140
+ export function* placed(animation, options, draw) {
139
141
  const t = frameTimeOf(options);
140
142
  const shown = shownAt(animation, t, options);
141
- const value = drawable(shown.value);
142
143
  const hold = holdOf(shown, t);
143
144
  options.onHold?.(hold);
144
- yield* stampFrame(render(value, options), hold);
145
+ yield* stampFrame(draw(shown.value), hold);
146
+ }
147
+ /**
148
+ * An animation's face, `render`: `placed`, its value drawn as the renderable
149
+ * it is. A container — the library's or a user's — that draws its children
150
+ * through `render` draws an animation among them with no word about time:
151
+ * the hold goes back the way the time came in. [LAW:composability]
152
+ */
153
+ export function placedRender(animation, options) {
154
+ return placed(animation, options, (value) => render(drawable(value), options));
145
155
  }
146
156
  /**
147
157
  * An animation's face, `measure`: what it shows at the frame's time, measured,
@@ -191,6 +201,17 @@ export const Animation = {
191
201
  });
192
202
  return faced;
193
203
  },
204
+ /**
205
+ * Whether `animation` never changes again from `t`: a one-shot that has
206
+ * run, a loop at a depth that shows no motion. [LAW:single-enforcer] The
207
+ * one reading of "never": a `Transition`'s `done` and a lifecycle's
208
+ * arrived and gone moments are this, so what ends a one-shot is written
209
+ * once. An early answer the contract allows is a finite one, so `Infinity`
210
+ * is only ever a true never.
211
+ */
212
+ done(animation, t) {
213
+ return animation.nextChange(t) === Number.POSITIVE_INFINITY;
214
+ },
194
215
  /** `value` at every time, never changing. */
195
216
  constant(value) {
196
217
  const still = Animation.of({ at: () => value, nextChange: () => Number.POSITIVE_INFINITY, lastChange: () => Number.NEGATIVE_INFINITY, reduced: () => still });
@@ -2,6 +2,7 @@
2
2
  * Terminal color representation, parsing, downgrading, and ANSI code generation.
3
3
  */
4
4
  import type { Env } from "./env.js";
5
+ import type { Palette } from "../themes/palette.js";
5
6
  /**
6
7
  * Immutable RGBA color value. Alpha defaults to 1 (fully opaque).
7
8
  *
@@ -244,7 +245,7 @@ export declare class ColorSpec {
244
245
  /**
245
246
  * Resolves to an actual RGB(A) value for any color type.
246
247
  */
247
- getTruecolor(theme?: TerminalTheme, foreground?: boolean): ColorRgba;
248
+ getTruecolor(theme?: TerminalColors, foreground?: boolean): ColorRgba;
248
249
  static default(): ColorSpec;
249
250
  static fromAnsi(n: number): ColorSpec;
250
251
  static fromRgba(value: ColorRgba): ColorSpec;
@@ -275,7 +276,7 @@ export declare class ColorSpec {
275
276
  * `value` is opaque: a translucent colour has no one look until it is laid
276
277
  * on a ground (`flattenAlpha`).
277
278
  */
278
- static matchOn(value: ColorRgba, depth: ColorDepth, theme: TerminalTheme, foreground: boolean): ColorSpec;
279
+ static matchOn(value: ColorRgba, depth: ColorDepth, theme: TerminalColors, foreground: boolean): ColorSpec;
279
280
  /**
280
281
  * Every colour `depth` writes by index, each with the colour a terminal
281
282
  * known to show `theme` draws it in, lowest index first: the theme's
@@ -285,7 +286,7 @@ export declare class ColorSpec {
285
286
  * depth only the default, so neither writes any by index. The colours
286
287
  * `matchOn` rounds to, but for the default, read off the same two tables.
287
288
  */
288
- static indexed(depth: ColorDepth, theme: TerminalTheme): readonly (readonly [ColorSpec, ColorRgba])[];
289
+ static indexed(depth: ColorDepth, theme: TerminalColors): readonly (readonly [ColorSpec, ColorRgba])[];
289
290
  /**
290
291
  * The spec a slot is written in once its colour moves from `was`, what its
291
292
  * written spec draws on `theme`, to the opaque `now`: `matchOn`'s — or
@@ -294,7 +295,7 @@ export declare class ColorSpec {
294
295
  * off any colour the depth can write — a theme slot, the default colour,
295
296
  * the cube — keeps the spec it was written in.
296
297
  */
297
- static matchMove(was: ColorRgba, now: ColorRgba, depth: ColorDepth, theme: TerminalTheme, foreground: boolean): ColorSpec | undefined;
298
+ static matchMove(was: ColorRgba, now: ColorRgba, depth: ColorDepth, theme: TerminalColors, foreground: boolean): ColorSpec | undefined;
298
299
  /**
299
300
  * Parse a color string. Memoized (`Memo`): a string parsed again while
300
301
  * remembered returns the same instance.
@@ -312,6 +313,17 @@ export declare function parseRgbaHex(hex: string): ColorRgba;
312
313
  */
313
314
  export declare function parseHexColor(literal: string): ColorRgba;
314
315
  export declare function blendRgb(color1: ColorRgba, color2: ColorRgba, crossFade?: number): ColorRgba;
316
+ /**
317
+ * What a terminal draws with: the part of a `TerminalTheme` a colour resolves
318
+ * against.
319
+ */
320
+ export declare class TerminalColors {
321
+ readonly backgroundColor: ColorRgba;
322
+ readonly foregroundColor: ColorRgba;
323
+ readonly ansiColors: ColorTable;
324
+ private readonly checked;
325
+ constructor(backgroundColor: ColorRgba, foregroundColor: ColorRgba, ansiColors: ColorTable);
326
+ }
315
327
  /**
316
328
  * A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
317
329
  * and a semantic palette.
@@ -321,12 +333,9 @@ export declare function blendRgb(color1: ColorRgba, color2: ColorRgba, crossFade
321
333
  * is ANSI colour 1 under every theme; which red that is, is the theme's to say,
322
334
  * as it is in Rich, where each `TerminalTheme` carries its own table.
323
335
  */
324
- export declare class TerminalTheme {
325
- readonly backgroundColor: ColorRgba;
326
- readonly foregroundColor: ColorRgba;
327
- readonly ansiColors: ColorTable;
328
- readonly palette: import("../themes/palette.js").Palette;
329
- constructor(backgroundColor: ColorRgba, foregroundColor: ColorRgba, ansiColors: ColorTable, palette: import("../themes/palette.js").Palette);
336
+ export declare class TerminalTheme extends TerminalColors {
337
+ readonly palette: Palette;
338
+ constructor(backgroundColor: ColorRgba, foregroundColor: ColorRgba, ansiColors: ColorTable, palette: Palette);
330
339
  }
331
340
  export declare const STANDARD_TABLE: ColorTable;
332
341
  export declare const EIGHT_BIT_TABLE: ColorTable;
@@ -345,7 +354,7 @@ export declare const WINDOWS_TABLE: ColorTable;
345
354
  * that reads it. (Widgets fall back to `DEFAULT_TERMINAL_THEME`, which carries a
346
355
  * palette this does not.)
347
356
  */
348
- export declare function resolveTerminal(theme: TerminalTheme | undefined, depth: ColorDepth): TerminalTheme;
357
+ export declare function resolveTerminal(theme: TerminalColors | undefined, depth: ColorDepth): TerminalColors;
349
358
  /**
350
359
  * The table a downgrade to `depth` rounds a fixed colour against.
351
360
  * [LAW:one-source-of-truth] `ColorSpec.downgrade` picks the index a colour is
@@ -3,9 +3,6 @@
3
3
  */
4
4
  import { Memo, WeakMemo } from "./memo.js";
5
5
  import { pattern } from "./pattern.js";
6
- // Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
7
- // only `import type`s from `core/color.ts`, so there is no runtime cycle.
8
- import { Palette } from "../themes/palette.js";
9
6
  // --- ColorRgba ---
10
7
  function hex2(byte) {
11
8
  return byte.toString(16).padStart(2, "0");
@@ -844,24 +841,17 @@ export function blendRgb(color1, color2, crossFade = 0.5) {
844
841
  }
845
842
  // --- TerminalTheme ---
846
843
  /**
847
- * A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
848
- * and a semantic palette.
849
- *
850
- * **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
851
- * showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
852
- * is ANSI colour 1 under every theme; which red that is, is the theme's to say,
853
- * as it is in Rich, where each `TerminalTheme` carries its own table.
844
+ * What a terminal draws with: the part of a `TerminalTheme` a colour resolves
845
+ * against.
854
846
  */
855
- export class TerminalTheme {
847
+ export class TerminalColors {
856
848
  backgroundColor;
857
849
  foregroundColor;
858
850
  ansiColors;
859
- palette;
860
- constructor(backgroundColor, foregroundColor, ansiColors, palette) {
851
+ constructor(backgroundColor, foregroundColor, ansiColors) {
861
852
  this.backgroundColor = backgroundColor;
862
853
  this.foregroundColor = foregroundColor;
863
854
  this.ansiColors = ansiColors;
864
- this.palette = palette;
865
855
  // [LAW:parse-dont-validate] Every reader indexes `ansiColors` 0–15 as the
866
856
  // terminal's sixteen, so a theme is the proof it holds exactly those.
867
857
  if (ansiColors.firstIndex !== 0 || ansiColors.size !== 16) {
@@ -869,6 +859,22 @@ export class TerminalTheme {
869
859
  }
870
860
  }
871
861
  }
862
+ /**
863
+ * A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
864
+ * and a semantic palette.
865
+ *
866
+ * **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
867
+ * showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
868
+ * is ANSI colour 1 under every theme; which red that is, is the theme's to say,
869
+ * as it is in Rich, where each `TerminalTheme` carries its own table.
870
+ */
871
+ export class TerminalTheme extends TerminalColors {
872
+ palette;
873
+ constructor(backgroundColor, foregroundColor, ansiColors, palette) {
874
+ super(backgroundColor, foregroundColor, ansiColors);
875
+ this.palette = palette;
876
+ }
877
+ }
872
878
  // --- ColorTable data ---
873
879
  function buildStandard16() {
874
880
  return [
@@ -943,7 +949,7 @@ export const EIGHT_BIT_TABLE = new ColorTable(build256Table());
943
949
  */
944
950
  export const EIGHT_BIT_DOWNGRADE_TABLE = new ColorTable(build256Table().slice(16), 16);
945
951
  export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
946
- // --- Internal fallback theme ---
952
+ // --- Internal fallback terminals ---
947
953
  //
948
954
  // [LAW:one-way-deps] Preset themes (DEFAULT_TERMINAL_THEME, MONOKAI, NORD, ...)
949
955
  // live in `src/themes/terminalThemes.ts` because they need `buildPalette` from
@@ -951,12 +957,11 @@ export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
951
957
  // `themes/* -> core/color`, with no runtime back-edge.
952
958
  //
953
959
  // `getTruecolor()` retains a fallback for callers that omit `theme`: the
954
- // terminal a colour's depth assumes, built from that depth's own sixteen with
955
- // an empty Palette — sufficient for the lookups it serves, which never read
956
- // `palette`.
957
- const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
960
+ // terminal a colour's depth assumes, its own sixteen and no palette, which the
961
+ // lookups it serves never read.
962
+ const INTERNAL_DEFAULT_TERMINAL = new TerminalColors(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE);
958
963
  /** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
959
- const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE, new Palette("windows", true, new Map()));
964
+ const WINDOWS_CONSOLE_TERMINAL = new TerminalColors(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE);
960
965
  /**
961
966
  * The terminal a colour at `depth` is drawn by: `theme`, or when a caller names
962
967
  * none, the one the depth assumes — the Windows console at WINDOWS, the VGA
@@ -965,7 +970,7 @@ const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TA
965
970
  * palette this does not.)
966
971
  */
967
972
  export function resolveTerminal(theme, depth) {
968
- return theme ?? (depth === ColorDepth.WINDOWS ? WINDOWS_CONSOLE_THEME : INTERNAL_DEFAULT_THEME);
973
+ return theme ?? (depth === ColorDepth.WINDOWS ? WINDOWS_CONSOLE_TERMINAL : INTERNAL_DEFAULT_TERMINAL);
969
974
  }
970
975
  /**
971
976
  * The table a downgrade to `depth` rounds a fixed colour against.
@@ -10,6 +10,7 @@ import { type JSONOptions } from "./json.js";
10
10
  import type { Highlighter } from "./highlighter.js";
11
11
  import { type RuleOptions } from "../renderables/rule.js";
12
12
  import type { Height, OverflowMethod, RenderOptions, StyleErrorHandler } from "./protocol.js";
13
+ import { type HostFrame } from "./protocol.js";
13
14
  import type { Unsubscribe } from "./subscription.js";
14
15
  export interface ConsoleSink {
15
16
  write(chunk: string | Uint8Array): unknown;
@@ -125,6 +126,13 @@ export interface ConsoleOptions {
125
126
  }
126
127
  export interface PrintOptions {
127
128
  style?: string | Style;
129
+ /**
130
+ * The frame this print is drawn in (`RenderOptions.frame`): the moment
131
+ * everything in it that moves is drawn for, named by the caller — a host's
132
+ * `frameNow()`. [LAW:single-enforcer] The one place a print's time is
133
+ * decided; absent, the print is drawn for no moment, 0 (`frameTimeOf`).
134
+ */
135
+ frame?: HostFrame;
128
136
  justify?: "default" | "left" | "center" | "right" | "full";
129
137
  overflow?: OverflowMethod;
130
138
  highlight?: boolean;
@@ -317,7 +317,7 @@ export class Console {
317
317
  !isRenderable(lastArg) &&
318
318
  ("style" in lastArg || "justify" in lastArg || "markup" in lastArg ||
319
319
  "highlight" in lastArg || "overflow" in lastArg || "end" in lastArg ||
320
- "softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg)) {
320
+ "softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg || "frame" in lastArg)) {
321
321
  opts = lastArg;
322
322
  items = args.slice(0, -1);
323
323
  }
@@ -407,6 +407,7 @@ export class Console {
407
407
  const renderOpts = {
408
408
  ...options,
409
409
  ...strings,
410
+ frame: opts.frame,
410
411
  justify: opts.justify === "default" ? undefined : opts.justify,
411
412
  overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
412
413
  noWrap: softWrap,
@@ -379,7 +379,12 @@ export function passAt(what, t, period) {
379
379
  // takes off; carrying a pass's start out of `t` itself would leave each
380
380
  // sample its own rounding. `+ 0` turns the `-0` a small negative `t` rounds
381
381
  // to into pass 0, so the pass is a plain integer whoever reads it.
382
- return { pass: Math.round((t - into) / period) + 0, into };
382
+ const pass = Math.round((t - into) / period) + 0;
383
+ // [LAW:no-silent-failure] Past where a pass is a float, `period` is finer than time resolves: the refusal a
384
+ // boundary walked there gives, said of `t` and not of the infinite sum a pass that overflowed would build.
385
+ if (!Number.isFinite(pass))
386
+ throw new RangeError(`${what}: a period of ${period} s is finer than time resolves near t = ${t}`);
387
+ return { pass, into };
383
388
  }
384
389
  /** The least normal double, 2^-1022: below it every double is a multiple of `Number.MIN_VALUE`. */
385
390
  const MIN_NORMAL = 2 ** -1022;
@@ -64,6 +64,8 @@ export declare const INVERT_LIGHTNESS: ThemeKey;
64
64
  * the predicate lives once, here.
65
65
  */
66
66
  export declare function isIdentityKey(k: ThemeKey): boolean;
67
+ export declare function srgbToLinear(v: number): number;
68
+ export declare function linearToSrgb(v: number): number;
67
69
  /**
68
70
  * Immutable OKLCH color. `l` ∈ [0, 1] (lightness), `c` ≥ 0 (chroma; typical
69
71
  * sRGB-representable colors top out near 0.32), `h` ∈ [0, 360) (hue
@@ -57,12 +57,12 @@ export function isIdentityKey(k) {
57
57
  k.lightnessShift === 0);
58
58
  }
59
59
  // ---------------------------------------------------------------------------
60
- // Pure conversion helpers (private).
60
+ // Pure conversion helpers.
61
61
  // ---------------------------------------------------------------------------
62
- function srgbToLinear(v) {
62
+ export function srgbToLinear(v) {
63
63
  return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
64
64
  }
65
- function linearToSrgb(v) {
65
+ export function linearToSrgb(v) {
66
66
  return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;
67
67
  }
68
68
  function clamp01(v) {
@@ -18,13 +18,27 @@ import type { Highlighter } from "./highlighter.js";
18
18
  */
19
19
  export type OverflowMethod = "fold" | "crop" | "ellipsis" | "ignore";
20
20
  /**
21
- * What draws frames — a `Live`, an `App` — as what draws in them sees it:
22
- * the time a frame drawn now is drawn for, the host's clock read through its
23
- * timeline for a view on it (`frameTime`, `./view.ts`). Not a `Clock`: under
24
- * a paused timeline it holds, and under a reversed one it runs backwards.
21
+ * The frame being drawn, as a value: `time`, the moment it is drawn for, in
22
+ * seconds, on the clock its cells' `nextChange` stamps are read on, and
23
+ * `host`, the identity of what drew it — nothing to read, so what draws in
24
+ * the frame has one time, this one, and cannot ask the host for another.
25
+ * [LAW:types-are-the-program] A `Progress` keeps the identity to refuse a
26
+ * frame of a host other than the one its tasks are timed on.
27
+ */
28
+ export interface HostFrame {
29
+ readonly time: number;
30
+ readonly host: object;
31
+ }
32
+ /**
33
+ * What draws frames — a `Live`, an `App` — as what runs between them sees
34
+ * it: the frame it would draw now, its time the host's clock read through
35
+ * its timeline (`frameNow`, `./view.ts`). Not a `Clock`: under a paused
36
+ * timeline it holds, and under a reversed one it runs backwards. Asked
37
+ * inside a render it throws (`RenderClockError`): a frame has the time it was
38
+ * drawn for, and a renderable reads that one.
25
39
  */
26
40
  export interface FrameHost {
27
- readonly frameTime: number;
41
+ frameNow(): HostFrame;
28
42
  }
29
43
  export interface RenderOptions {
30
44
  /**
@@ -105,18 +119,14 @@ export interface RenderOptions {
105
119
  */
106
120
  reducedMotion?: boolean;
107
121
  /**
108
- * The frame being drawn: `time`, the moment it is drawn for, in seconds, on
109
- * the clock its cells' `nextChange` stamps are read on, and `host`, the
110
- * host drawing it, whose `frameTime` that moment was. It is how a
111
- * renderable that lives across frames — a widget, a `Progress` — draws for
112
- * the time without being rebuilt each frame with one, and without reading
113
- * a clock. Absent, the frame is drawn for no moment — a `print` — and
114
- * nothing drawn from it moves.
122
+ * The frame being drawn (`HostFrame`): how a renderable that lives across
123
+ * frames — a widget, a `Progress` — draws for the time without being
124
+ * rebuilt each frame with one, and without reading a clock. A host writes
125
+ * it once a frame, and a print writes the one its caller named
126
+ * (`PrintOptions.frame`). Absent, the frame is drawn for no moment, 0
127
+ * (`frameTimeOf`, `./animation.ts`), and nothing drawn from it moves.
115
128
  */
116
- frame?: {
117
- readonly time: number;
118
- readonly host: FrameHost;
119
- };
129
+ frame?: HostFrame;
120
130
  /**
121
131
  * Told of every renderable `render` is asked for, as it is asked, so the
122
132
  * calls arrive in document order: a container before what it holds, what
@@ -49,7 +49,7 @@ import type { ColorSystemName, Destination } from "./color.js";
49
49
  import type { Env } from "./env.js";
50
50
  import { Segment } from "./segment.js";
51
51
  import type { Renderable, RenderOptions } from "./protocol.js";
52
- export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
52
+ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "reducedMotion" | "theme" | "onStyleError" | "highlight" | "highlighter" | "frame"> {
53
53
  /** Cell width to render into. Default 80. */
54
54
  width?: number;
55
55
  /**
@@ -118,6 +118,7 @@ export function renderToString(renderable, options) {
118
118
  onStyleError: options?.onStyleError,
119
119
  highlight: options?.highlight,
120
120
  highlighter: options?.highlighter,
121
+ frame: options?.frame,
121
122
  // [LAW:one-source-of-truth] The depth the segments below are encoded at,
122
123
  // so a renderable measures what this very call will draw.
123
124
  colorSystem: destination.colorSystem,
@@ -36,7 +36,7 @@
36
36
  * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
37
37
  * [LAW:one-way-deps]
38
38
  */
39
- import { type FrameHost, type Renderable, type RenderOptions } from "./protocol.js";
39
+ import { type HostFrame, type Renderable, type RenderOptions } from "./protocol.js";
40
40
  import { Segment } from "./segment.js";
41
41
  import { Timeline } from "./timeline.js";
42
42
  /**
@@ -61,12 +61,26 @@ export interface HostTime {
61
61
  /**
62
62
  * The time a frame drawn at `time` is drawn for, on the clock its cells are
63
63
  * stamped on: the time the timeline maps `now` to. [LAW:single-enforcer]
64
- * `drawView` draws at it and hands it to the frame as
65
- * `RenderOptions.frame`, and a host reads it here for anything heard between
66
- * frames — a key — so a moment a widget keeps and the frame it is compared
67
- * against are on one clock.
64
+ * `frameNow` puts it on the frame `drawView` draws and on the one a host
65
+ * hands out between frames (`frameNow`) — what a key is stamped with — so a
66
+ * moment a widget keeps and the frame it is compared against are on one
67
+ * clock.
68
68
  */
69
69
  export declare function frameTime(time: HostTime): number;
70
+ /** A host's frame was asked for during a render or measure. */
71
+ export declare class RenderClockError extends Error {
72
+ constructor();
73
+ }
74
+ /**
75
+ * The frame `host` draws now, at `time`: what every host hands out as
76
+ * `frameNow()`, and what `drawView` draws. [LAW:single-enforcer] The one
77
+ * place a host's time is read for a frame, so the one place it is refused
78
+ * inside a render or measure (`RenderClockError`): a renderable drawing in a
79
+ * frame has that frame's time and asks nothing else, as `State.set` is
80
+ * refused there (`./state.ts`). A frame is a value with no clock on it, so a
81
+ * renderable handed one cannot read a second, moving time mid-render.
82
+ */
83
+ export declare function frameNow(host: object, time: HostTime): HostFrame;
70
84
  /** What a frame drawn at host time `t` says about when it changes. */
71
85
  export interface FrameSays {
72
86
  /** The host time the frame was drawn at. */
@@ -86,7 +100,7 @@ export declare const undrawn: (t: number) => FrameSays;
86
100
  * overflow — and when the frame changes on the host's clock: the hold its
87
101
  * cells say, carried back through the timeline. [LAW:one-source-of-truth]
88
102
  * `options` is the one place the frame reads the reader's preference for
89
- * motion, handed on to `draw` with the frame: the time it is drawn for
90
- * (`frameTime`) and `drawnBy`, the host whose time that is.
103
+ * motion, handed on to `draw` with the frame (`frameNow`): the time it is
104
+ * drawn for and `drawnBy`, the host whose time that is.
91
105
  */
92
- export declare function drawView(view: Renderable, time: HostTime, drawnBy: FrameHost, options: RenderOptions, draw: (renderable: Renderable, options: RenderOptions) => Segment[][]): DrawnView;
106
+ export declare function drawView(view: Renderable, time: HostTime, drawnBy: object, options: RenderOptions, draw: (renderable: Renderable, options: RenderOptions) => Segment[][]): DrawnView;
package/dist/core/view.js CHANGED
@@ -36,7 +36,7 @@
36
36
  * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
37
37
  * [LAW:one-way-deps]
38
38
  */
39
- import { render } from "./protocol.js";
39
+ import { activePass, render } from "./protocol.js";
40
40
  import { Segment } from "./segment.js";
41
41
  import { Timeline } from "./timeline.js";
42
42
  /** `view` as the renderable a host holds: a function is asked each time it is drawn. */
@@ -46,14 +46,36 @@ export function hostView(view) {
46
46
  /**
47
47
  * The time a frame drawn at `time` is drawn for, on the clock its cells are
48
48
  * stamped on: the time the timeline maps `now` to. [LAW:single-enforcer]
49
- * `drawView` draws at it and hands it to the frame as
50
- * `RenderOptions.frame`, and a host reads it here for anything heard between
51
- * frames — a key — so a moment a widget keeps and the frame it is compared
52
- * against are on one clock.
49
+ * `frameNow` puts it on the frame `drawView` draws and on the one a host
50
+ * hands out between frames (`frameNow`) — what a key is stamped with — so a
51
+ * moment a widget keeps and the frame it is compared against are on one
52
+ * clock.
53
53
  */
54
54
  export function frameTime(time) {
55
55
  return Timeline.at(time.timeline, time.now);
56
56
  }
57
+ /** A host's frame was asked for during a render or measure. */
58
+ export class RenderClockError extends Error {
59
+ constructor() {
60
+ super("a host's frame was asked for during a render or measure. A frame has one time, the one it is drawn for: " +
61
+ "read it from the render options' `frame`, or ask the host between frames.");
62
+ this.name = "RenderClockError";
63
+ }
64
+ }
65
+ /**
66
+ * The frame `host` draws now, at `time`: what every host hands out as
67
+ * `frameNow()`, and what `drawView` draws. [LAW:single-enforcer] The one
68
+ * place a host's time is read for a frame, so the one place it is refused
69
+ * inside a render or measure (`RenderClockError`): a renderable drawing in a
70
+ * frame has that frame's time and asks nothing else, as `State.set` is
71
+ * refused there (`./state.ts`). A frame is a value with no clock on it, so a
72
+ * renderable handed one cannot read a second, moving time mid-render.
73
+ */
74
+ export function frameNow(host, time) {
75
+ if (activePass() !== undefined)
76
+ throw new RenderClockError();
77
+ return { time: frameTime(time), host };
78
+ }
57
79
  /** What a frame not yet drawn at host time `t` says: nothing. */
58
80
  export const undrawn = (t) => ({ t, nextChange: Number.POSITIVE_INFINITY });
59
81
  /** The hold of the frame `lines` draw, told `told` besides: the intersection of its cells' holds and it. */
@@ -64,8 +86,8 @@ const frameHold = (lines, told) => lines.reduce((hold, row) => ({ lastChange: Ma
64
86
  * overflow — and when the frame changes on the host's clock: the hold its
65
87
  * cells say, carried back through the timeline. [LAW:one-source-of-truth]
66
88
  * `options` is the one place the frame reads the reader's preference for
67
- * motion, handed on to `draw` with the frame: the time it is drawn for
68
- * (`frameTime`) and `drawnBy`, the host whose time that is.
89
+ * motion, handed on to `draw` with the frame (`frameNow`): the time it is
90
+ * drawn for and `drawnBy`, the host whose time that is.
69
91
  */
70
92
  export function drawView(view, time, drawnBy, options, draw) {
71
93
  const { now: host, timeline } = time;
@@ -73,7 +95,9 @@ export function drawView(view, time, drawnBy, options, draw) {
73
95
  const onHold = (hold) => {
74
96
  told = { lastChange: Math.max(told.lastChange, hold.lastChange), nextChange: Math.min(told.nextChange, hold.nextChange) };
75
97
  };
76
- const lines = draw(view, { ...options, frame: { time: frameTime(time), host: drawnBy }, onHold });
98
+ // Made before the render opens: the one time the frame has.
99
+ const frame = frameNow(drawnBy, time);
100
+ const lines = draw(view, { ...options, frame, onHold });
77
101
  const hold = frameHold(lines, told);
78
102
  // The frame as the sample of the view it is, on the timeline's clock; the
79
103
  // timeline says when the host's clock leaves its hold.
@@ -34,7 +34,7 @@ import { type Clock, type FrameRate } from "../core/clock.js";
34
34
  import { type ConsoleOptions } from "../core/console.js";
35
35
  import { Segment } from "../core/segment.js";
36
36
  import { type Surface } from "../core/paint.js";
37
- import { type FrameHost } from "../core/protocol.js";
37
+ import { type HostFrame, type FrameHost } from "../core/protocol.js";
38
38
  import type { Unsubscribe } from "../core/subscription.js";
39
39
  import { Timeline } from "../core/timeline.js";
40
40
  import { type View } from "../core/view.js";
@@ -149,12 +149,12 @@ export declare class App implements FrameHost {
149
149
  /** The rows on screen now, as painted: what a pointer event lands on. */
150
150
  get frame(): readonly (readonly Segment[])[];
151
151
  /**
152
- * The time a frame drawn now is drawn for (`RenderOptions.frame`): the
153
- * clock's now, through the timeline. What a moment heard
152
+ * The frame this app draws now (`FrameHost`): the clock's now, through the
153
+ * timeline, on a frame it is the host of. Its time is what a moment heard
154
154
  * between frames — a key — is stamped with, so it is on the clock the
155
155
  * frame is.
156
156
  */
157
- get frameTime(): number;
157
+ frameNow(): HostFrame;
158
158
  private _time;
159
159
  /**
160
160
  * Take the terminal, paint the first frame, and resolve when the app stops
package/dist/host/app.js CHANGED
@@ -36,7 +36,7 @@ import { Segment } from "../core/segment.js";
36
36
  import { SurfacePainter } from "../core/paint.js";
37
37
  import { fitHeight, render } from "../core/protocol.js";
38
38
  import { Timeline } from "../core/timeline.js";
39
- import { drawView, frameTime, hostView, undrawn } from "../core/view.js";
39
+ import { drawView, frameNow, hostView, undrawn } from "../core/view.js";
40
40
  import { hostEnvironment } from "./host-environment.js";
41
41
  import { InputParser, keyName, parseKeyName } from "./input.js";
42
42
  // [LAW:one-source-of-truth] How a frame draws — its glyphs, the style names
@@ -153,13 +153,13 @@ export class App {
153
153
  return this._frame;
154
154
  }
155
155
  /**
156
- * The time a frame drawn now is drawn for (`RenderOptions.frame`): the
157
- * clock's now, through the timeline. What a moment heard
156
+ * The frame this app draws now (`FrameHost`): the clock's now, through the
157
+ * timeline, on a frame it is the host of. Its time is what a moment heard
158
158
  * between frames — a key — is stamped with, so it is on the clock the
159
159
  * frame is.
160
160
  */
161
- get frameTime() {
162
- return frameTime(this._time());
161
+ frameNow() {
162
+ return frameNow(this, this._time());
163
163
  }
164
164
  _time() {
165
165
  return { now: this._clock.now(), epoch: this._epoch, timeline: this._timeline };