@promptctl/rich-js 0.26.0 → 0.28.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 (132) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +52 -15
  4. package/dist/core/animation.js +82 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +93 -5
  17. package/dist/core/color.d.ts +26 -18
  18. package/dist/core/color.js +37 -34
  19. package/dist/core/console.d.ts +26 -1
  20. package/dist/core/console.js +26 -6
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/easing.js +6 -1
  24. package/dist/core/env.d.ts +6 -0
  25. package/dist/core/env.js +9 -1
  26. package/dist/core/export-html.js +4 -3
  27. package/dist/core/export-lines.d.ts +2 -0
  28. package/dist/core/export-lines.js +7 -3
  29. package/dist/core/export-svg.js +6 -5
  30. package/dist/core/json.js +9 -2
  31. package/dist/core/markup.js +4 -3
  32. package/dist/core/oklch.d.ts +2 -0
  33. package/dist/core/oklch.js +3 -3
  34. package/dist/core/orient.d.ts +68 -0
  35. package/dist/core/orient.js +224 -0
  36. package/dist/core/osc8.d.ts +12 -5
  37. package/dist/core/osc8.js +14 -7
  38. package/dist/core/pretty.js +22 -18
  39. package/dist/core/protocol.d.ts +53 -22
  40. package/dist/core/protocol.js +14 -6
  41. package/dist/core/render.d.ts +8 -3
  42. package/dist/core/render.js +19 -5
  43. package/dist/core/seal.d.ts +26 -0
  44. package/dist/core/seal.js +75 -0
  45. package/dist/core/segment.d.ts +9 -1
  46. package/dist/core/segment.js +11 -1
  47. package/dist/core/style.d.ts +29 -1
  48. package/dist/core/style.js +41 -5
  49. package/dist/core/text-order.d.ts +51 -0
  50. package/dist/core/text-order.js +50 -0
  51. package/dist/core/text.d.ts +10 -0
  52. package/dist/core/text.js +133 -87
  53. package/dist/core/timeline.js +6 -6
  54. package/dist/core/view.d.ts +23 -9
  55. package/dist/core/view.js +37 -12
  56. package/dist/core/wrap.d.ts +24 -6
  57. package/dist/core/wrap.js +82 -16
  58. package/dist/host/app.d.ts +48 -7
  59. package/dist/host/app.js +116 -11
  60. package/dist/host/index.d.ts +3 -2
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +35 -2
  63. package/dist/host/input.js +88 -4
  64. package/dist/host/terminal-colors.d.ts +18 -0
  65. package/dist/host/terminal-colors.js +46 -0
  66. package/dist/host/terminal-host.d.ts +2 -1
  67. package/dist/index.d.ts +10 -8
  68. package/dist/index.js +2 -2
  69. package/dist/renderables/change.d.ts +1 -3
  70. package/dist/renderables/change.js +4 -6
  71. package/dist/renderables/effect.d.ts +61 -60
  72. package/dist/renderables/effect.js +91 -81
  73. package/dist/renderables/effects.d.ts +37 -8
  74. package/dist/renderables/effects.js +193 -55
  75. package/dist/renderables/frames.d.ts +1 -10
  76. package/dist/renderables/frames.js +1 -16
  77. package/dist/renderables/glyph-effect.d.ts +12 -13
  78. package/dist/renderables/glyph-effect.js +34 -32
  79. package/dist/renderables/glyph-effects.d.ts +27 -8
  80. package/dist/renderables/glyph-effects.js +75 -7
  81. package/dist/renderables/lifecycle.d.ts +40 -32
  82. package/dist/renderables/lifecycle.js +87 -48
  83. package/dist/renderables/live.d.ts +3 -3
  84. package/dist/renderables/live.js +4 -4
  85. package/dist/renderables/markdown.js +4 -2
  86. package/dist/renderables/panel.js +6 -4
  87. package/dist/renderables/progress.d.ts +11 -4
  88. package/dist/renderables/progress.js +29 -17
  89. package/dist/renderables/progressBar.d.ts +3 -3
  90. package/dist/renderables/progressBar.js +16 -13
  91. package/dist/renderables/syntax.js +7 -2
  92. package/dist/renderables/table.js +2 -1
  93. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  94. package/dist/template-bindings/animation-funcs.js +115 -32
  95. package/dist/template-bindings/index.d.ts +7 -5
  96. package/dist/template-bindings/index.js +8 -5
  97. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  98. package/dist/template-bindings/palette-funcs.js +1 -1
  99. package/dist/themes/buildPalette.d.ts +15 -39
  100. package/dist/themes/buildPalette.js +196 -67
  101. package/dist/themes/colorMath.d.ts +10 -4
  102. package/dist/themes/colorMath.js +33 -1
  103. package/dist/themes/data/index.js +1 -4
  104. package/dist/themes/data/textual-ansi.js +0 -143
  105. package/dist/themes/data/types.d.ts +3 -1
  106. package/dist/themes/palette.d.ts +22 -6
  107. package/dist/themes/palette.js +13 -21
  108. package/dist/themes/ramp.d.ts +1 -1
  109. package/dist/themes/ramp.js +1 -1
  110. package/dist/themes/registry.d.ts +7 -10
  111. package/dist/themes/registry.js +9 -30
  112. package/dist/themes/terminalThemes.d.ts +4 -10
  113. package/dist/themes/terminalThemes.js +31 -46
  114. package/dist/themes/transpose.d.ts +5 -10
  115. package/dist/themes/transpose.js +13 -24
  116. package/dist/widgets/button.d.ts +1 -1
  117. package/dist/widgets/button.js +6 -6
  118. package/dist/widgets/checkbox.js +4 -4
  119. package/dist/widgets/dropdown.d.ts +1 -1
  120. package/dist/widgets/dropdown.js +7 -5
  121. package/dist/widgets/event-router.js +2 -2
  122. package/dist/widgets/ink.d.ts +5 -4
  123. package/dist/widgets/ink.js +6 -9
  124. package/dist/widgets/text-input.d.ts +1 -1
  125. package/dist/widgets/text-input.js +41 -32
  126. package/dist/widgets/themed-widget.js +3 -3
  127. package/dist/widgets/toggle.d.ts +1 -1
  128. package/dist/widgets/toggle.js +5 -5
  129. package/dist/widgets/types.d.ts +2 -2
  130. package/dist/widgets/widget-app.d.ts +3 -3
  131. package/dist/widgets/widget-app.js +5 -5
  132. package/package.json +5 -4
package/README.md CHANGED
@@ -397,6 +397,7 @@ npm run demo
397
397
  | `npm run animation-playground` | animation-playground | Every kind of motion — tweens, moving ramps, a marching border, glyph and attribute effects, transitions, a list that comes and goes, a marquee, staggers and a blinking caret — on one timeline, with buttons to pause, reverse, scrub, change speed, reduce motion, drop to sixteen colours or none, and paint at one frame a second or sixty. |
398
398
  | `npm run markup-plugins` | rich-markup-plugins | Custom markup tags registered through `MarkupRegistry`. Prints once and exits. |
399
399
  | `npm run viewport` | rich-viewport | A selection you move through a list taller than its window with the arrow keys, `j`/`k`, PgUp/PgDn and Home/End, kept in view by `Viewport.ensureVisible`; the mouse wheel scrolls the window, and `q` quits. |
400
+ | `npm run terminal-colors` | terminal-colors | Your terminal asked for its own foreground and background with OSC 10 and OSC 11, shown as swatches, and whether it answered or the colours are assumed. |
400
401
  | `npm run terminal-host` | terminal-host | Each key you press printed as the bytes the terminal sends for it, and each resize as the new size — a program written against `TerminalHost` alone. `examples/terminal-host/embed.ts` runs the same program on an xterm.js terminal in your own page. |
401
402
 
402
403
  `Ctrl-C` quits any demo that stays running.
@@ -107,3 +107,45 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
107
107
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
108
108
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
109
109
  SOFTWARE.
110
+
111
+ --------------------------------------------------------------------------------
112
+
113
+ src/core/bidi-data.ts holds data derived from the Unicode Character Database.
114
+
115
+ UNICODE LICENSE V3
116
+
117
+ COPYRIGHT AND PERMISSION NOTICE
118
+
119
+ Copyright (c) 1991-2026 Unicode, Inc.
120
+
121
+ NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING,
122
+ INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR SOFTWARE, YOU
123
+ UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS
124
+ OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY,
125
+ DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
126
+
127
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
128
+ data files and any associated documentation (the "Data Files") or software and
129
+ any associated documentation (the "Software") to deal in the Data Files or
130
+ Software without restriction, including without limitation the rights to use,
131
+ copy, modify, merge, publish, distribute, and/or sell copies of the Data Files
132
+ or Software, and to permit persons to whom the Data Files or Software are
133
+ furnished to do so, provided that either (a) this copyright and permission
134
+ notice appear with all copies of the Data Files or Software, or (b) this
135
+ copyright and permission notice appear in associated Documentation.
136
+
137
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
138
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
139
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD
140
+ PARTY RIGHTS.
141
+
142
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE
143
+ LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY
144
+ DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
145
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
146
+ CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
147
+
148
+ Except as contained in this notice, the name of a copyright holder shall not be
149
+ used in advertising or otherwise to promote the sale, use or other dealings in
150
+ these Data Files or Software without prior written authorization of the
151
+ copyright holder.
@@ -9,7 +9,8 @@
9
9
  * changes every frame, however close together they are drawn. An early
10
10
  * answer costs a host a frame that draws what the last one did; a late one
11
11
  * skips a change. So an animation that cannot say answers `t`, as
12
- * `continuous` does. A value drawn as cells carries this answer on every
12
+ * `continuous` does. An answer that depends on what the frame drew, an
13
+ * effect's, is told by that drawing (`Moves`). A value drawn as cells carries this answer on every
13
14
  * cell (`Style.hold`, both ends of the hold below), so a host holding
14
15
  * the composed frame reads it there (`Segment.nextChange`); a value that is
15
16
  * not drawn — a number, a colour — has `nextChange` as its only way to say
@@ -76,14 +77,31 @@ import { type Beat } from "./easing.js";
76
77
  import { type Measurable, type Motion, type Renderable, type RenderOptions } from "./protocol.js";
77
78
  import { Segment } from "./segment.js";
78
79
  import { Style, type Hold } from "./style.js";
80
+ /**
81
+ * What a frame's drawing said of the hold the element named `key`, drawn at
82
+ * `t`, is in, or nothing if the frame heard of no such drawing: handed to an
83
+ * animation asked about a frame already drawn, so an answer only a drawing
84
+ * knows goes up through the combinators that remapped the time. It reads by
85
+ * `key` and not by the effect, which `reduced()` and a `repeat`'s `nth` build
86
+ * anew at each asking, and by `t`, since one element may be drawn at two
87
+ * moments. Parts sharing both share the intersection of what was drawn:
88
+ * narrower and never wider. A drawing a container hid, by not handing on its
89
+ * options, reads as none and stays early, unless a part sharing its key and
90
+ * `t` told. A measurement, which draws nothing, answers held for good for
91
+ * every part (`UNLAID`). An animation that does not read it ignores it, and
92
+ * one that forwards it to no part stays early. [LAW:one-source-of-truth] The
93
+ * frame's one drawing says it, so the answer cannot disagree with what the
94
+ * frame shows.
95
+ */
96
+ export type Moves = (t: number, key: string) => Hold | undefined;
79
97
  /** A value of `T` at a time in seconds, and the hold it is in: what every animation answers, whatever its values. */
80
98
  export interface Timed<T> {
81
99
  /** The value at `t` seconds. */
82
100
  at(t: number): T;
83
101
  /** The first moment, never before `t`, at which the value may stop being `at(t)`: `t` while it moves, `Infinity` once it never will. */
84
- nextChange(t: number): number;
102
+ nextChange(t: number, moves?: Moves): number;
85
103
  /** The last moment, never after `t`, at which the value may have become `at(t)`: `t` while it moves, `-Infinity` if it always was. */
86
- lastChange(t: number): number;
104
+ lastChange(t: number, moves?: Moves): number;
87
105
  /** This animation with its motion taken out — a loop at its first value, a one-shot cut to its end — drawn in its place under reduced motion. */
88
106
  reduced(): Animation<T>;
89
107
  }
@@ -99,8 +117,6 @@ export type Animation<T> = Timed<T> & ([T] extends [Renderable] ? Renderable & M
99
117
  /** What a frame drawn at a time shows of an animation, and what that says. */
100
118
  export interface Shown<T> {
101
119
  readonly value: T;
102
- /** The first moment, never before the time drawn at, the value may stop being this. */
103
- readonly nextChange: number;
104
120
  /** The animation the value is drawn from: the one handed in, or under reduced motion its reduced form. */
105
121
  readonly drawn: Timed<T>;
106
122
  /** Whether it is the animation reduced: drawn under reduced motion, changing only where content does. */
@@ -115,8 +131,8 @@ export interface Shown<T> {
115
131
  * reduces every one.
116
132
  */
117
133
  export declare function shownAt<T>(animation: Timed<T>, t: number, motion: Motion): Shown<T>;
118
- /** The hold `shown`, drawn at `t`, is in: its animation's two answers there. */
119
- export declare function holdOf(shown: Shown<unknown>, t: number): Hold;
134
+ /** The hold `shown`, drawn at `t`, is in: its animation's two answers there, given what its drawing said. */
135
+ export declare function holdOf(shown: Shown<unknown>, t: number, moves?: Moves): Hold;
120
136
  /** The style saying a cell is in `hold`. */
121
137
  export declare function stamp(hold: Hold): Style;
122
138
  /**
@@ -137,14 +153,26 @@ export declare function stampFrame(segments: Iterable<Segment>, hold: Hold): Ite
137
153
  */
138
154
  export declare function frameTimeOf(options: RenderOptions): number;
139
155
  /**
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]
156
+ * An animation placed in the frame `options` draws: its value at the frame's
157
+ * time (`frameTimeOf`, `shownAt`), drawn whole by `draw` under the options it
158
+ * is handed, every segment then stamped with its hold there under whatever
159
+ * its own cells say, and the hold told to the frame (`RenderOptions.onHold`),
160
+ * because a frame of no cells — an element not yet entered — may have cells
161
+ * later, and must say when without taking a row to say it in. Drawn first,
162
+ * because the hold is asked with what the drawing said (`Moves`); each report
163
+ * goes on outward too, since a value drawn inside a placement in this value
164
+ * may be a part of an animation placed further out. [LAW:single-enforcer]
165
+ * The one placement: an animation of renderables (`placedRender`), an effect
166
+ * and a widget drawing one cell from an animation of its own (a caret's
167
+ * blink) place through it, so how a time reaches an animation and how its
168
+ * hold reaches the frame is written once.
169
+ */
170
+ export declare function placed<T>(animation: Timed<T>, options: RenderOptions, draw: (value: T, options: RenderOptions) => Iterable<Segment>): Iterable<Segment>;
171
+ /**
172
+ * An animation's face, `render`: `placed`, its value drawn as the renderable
173
+ * it is. A container — the library's or a user's — that draws its children
174
+ * through `render` draws an animation among them with no word about time:
175
+ * the hold goes back the way the time came in. [LAW:composability]
148
176
  */
149
177
  export declare function placedRender(animation: Timed<unknown>, options: RenderOptions): Iterable<Segment>;
150
178
  /**
@@ -169,6 +197,15 @@ export declare const Animation: {
169
197
  * renderables throws when it is drawn.
170
198
  */
171
199
  of<X extends Timed<unknown>>(timed: X): Faced<NoInfer<X>>;
200
+ /**
201
+ * Whether `animation` never changes again from `t`: a one-shot that has
202
+ * run, a loop at a depth that shows no motion. [LAW:single-enforcer] The
203
+ * one reading of "never": a `Transition`'s `done` and a lifecycle's
204
+ * arrived and gone moments are this, so what ends a one-shot is written
205
+ * once. An early answer the contract allows is a finite one, so `Infinity`
206
+ * is only ever a true never.
207
+ */
208
+ done(animation: Timed<unknown>, t: number): boolean;
172
209
  /** `value` at every time, never changing. */
173
210
  constant<T>(value: T): Animation<T>;
174
211
  /**
@@ -9,7 +9,8 @@
9
9
  * changes every frame, however close together they are drawn. An early
10
10
  * answer costs a host a frame that draws what the last one did; a late one
11
11
  * skips a change. So an animation that cannot say answers `t`, as
12
- * `continuous` does. A value drawn as cells carries this answer on every
12
+ * `continuous` does. An answer that depends on what the frame drew, an
13
+ * effect's, is told by that drawing (`Moves`). A value drawn as cells carries this answer on every
13
14
  * cell (`Style.hold`, both ends of the hold below), so a host holding
14
15
  * the composed frame reads it there (`Segment.nextChange`); a value that is
15
16
  * not drawn — a number, a colour — has `nextChange` as its only way to say
@@ -76,7 +77,7 @@ import { beat, firstReaching, passAt, positiveSeconds } from "./easing.js";
76
77
  import { Measurement } from "./measure.js";
77
78
  import { isRenderable, render } from "./protocol.js";
78
79
  import { Segment } from "./segment.js";
79
- import { Style } from "./style.js";
80
+ import { intersection, NEVER_CHANGES, Style } from "./style.js";
80
81
  /**
81
82
  * What a frame drawn at `t` shows of `animation` under `motion`, and when
82
83
  * that changes: the animation's, or under reduced motion its reduced form's
@@ -88,11 +89,11 @@ import { Style } from "./style.js";
88
89
  export function shownAt(animation, t, motion) {
89
90
  const reduced = motion.reducedMotion === true;
90
91
  const drawn = reduced ? animation.reduced() : animation;
91
- return { value: drawn.at(t), nextChange: drawn.nextChange(t), drawn, reduced };
92
+ return { value: drawn.at(t), drawn, reduced };
92
93
  }
93
- /** The hold `shown`, drawn at `t`, is in: its animation's two answers there. */
94
- export function holdOf(shown, t) {
95
- return { lastChange: shown.drawn.lastChange(t), nextChange: shown.nextChange };
94
+ /** The hold `shown`, drawn at `t`, is in: its animation's two answers there, given what its drawing said. */
95
+ export function holdOf(shown, t, moves) {
96
+ return { lastChange: shown.drawn.lastChange(t, moves), nextChange: shown.drawn.nextChange(t, moves) };
96
97
  }
97
98
  /** The style saying a cell is in `hold`. */
98
99
  export function stamp(hold) {
@@ -126,22 +127,43 @@ function drawable(value) {
126
127
  return value;
127
128
  }
128
129
  /**
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]
130
+ * An animation placed in the frame `options` draws: its value at the frame's
131
+ * time (`frameTimeOf`, `shownAt`), drawn whole by `draw` under the options it
132
+ * is handed, every segment then stamped with its hold there under whatever
133
+ * its own cells say, and the hold told to the frame (`RenderOptions.onHold`),
134
+ * because a frame of no cells — an element not yet entered — may have cells
135
+ * later, and must say when without taking a row to say it in. Drawn first,
136
+ * because the hold is asked with what the drawing said (`Moves`); each report
137
+ * goes on outward too, since a value drawn inside a placement in this value
138
+ * may be a part of an animation placed further out. [LAW:single-enforcer]
139
+ * The one placement: an animation of renderables (`placedRender`), an effect
140
+ * and a widget drawing one cell from an animation of its own (a caret's
141
+ * blink) place through it, so how a time reaches an animation and how its
142
+ * hold reaches the frame is written once.
137
143
  */
138
- export function* placedRender(animation, options) {
144
+ export function* placed(animation, options, draw) {
139
145
  const t = frameTimeOf(options);
140
146
  const shown = shownAt(animation, t, options);
141
- const value = drawable(shown.value);
142
- const hold = holdOf(shown, t);
147
+ const said = new Map();
148
+ const part = (at, key) => `${at} ${key}`;
149
+ const onMoved = (at, key, hold) => {
150
+ const known = said.get(part(at, key));
151
+ said.set(part(at, key), known === undefined ? hold : intersection(known, hold));
152
+ options.onMoved?.(at, key, hold);
153
+ };
154
+ const segments = [...draw(shown.value, { ...options, onMoved })];
155
+ const hold = holdOf(shown, t, (at, key) => said.get(part(at, key)));
143
156
  options.onHold?.(hold);
144
- yield* stampFrame(render(value, options), hold);
157
+ yield* stampFrame(segments, hold);
158
+ }
159
+ /**
160
+ * An animation's face, `render`: `placed`, its value drawn as the renderable
161
+ * it is. A container — the library's or a user's — that draws its children
162
+ * through `render` draws an animation among them with no word about time:
163
+ * the hold goes back the way the time came in. [LAW:composability]
164
+ */
165
+ export function placedRender(animation, options) {
166
+ return placed(animation, options, (value, drawing) => render(drawable(value), drawing));
145
167
  }
146
168
  /**
147
169
  * An animation's face, `measure`: what it shows at the frame's time, measured,
@@ -151,9 +173,16 @@ export function* placedRender(animation, options) {
151
173
  export function placedMeasure(animation, options) {
152
174
  const t = frameTimeOf(options);
153
175
  const shown = shownAt(animation, t, options);
154
- options.onHold?.(holdOf(shown, t));
176
+ options.onHold?.(holdOf(shown, t, UNLAID));
155
177
  return Measurement.get(options, drawable(shown.value));
156
178
  }
179
+ /**
180
+ * What a measurement's drawing would say: held for good. An effect changes
181
+ * how cells look and never which exist, so nothing it does can change a
182
+ * layout; a combinator's own switches and a child's holds still reach the
183
+ * measurement. [LAW:types-are-the-program]
184
+ */
185
+ const UNLAID = () => NEVER_CHANGES;
157
186
  /**
158
187
  * Every member of `source`, its own and its class's, as own properties of a
159
188
  * new object: a method bound to `source`, a getter read off it live, a field
@@ -191,6 +220,17 @@ export const Animation = {
191
220
  });
192
221
  return faced;
193
222
  },
223
+ /**
224
+ * Whether `animation` never changes again from `t`: a one-shot that has
225
+ * run, a loop at a depth that shows no motion. [LAW:single-enforcer] The
226
+ * one reading of "never": a `Transition`'s `done` and a lifecycle's
227
+ * arrived and gone moments are this, so what ends a one-shot is written
228
+ * once. An early answer the contract allows is a finite one, so `Infinity`
229
+ * is only ever a true never.
230
+ */
231
+ done(animation, t) {
232
+ return animation.nextChange(t) === Number.POSITIVE_INFINITY;
233
+ },
194
234
  /** `value` at every time, never changing. */
195
235
  constant(value) {
196
236
  const still = Animation.of({ at: () => value, nextChange: () => Number.POSITIVE_INFINITY, lastChange: () => Number.NEGATIVE_INFINITY, reduced: () => still });
@@ -237,8 +277,8 @@ export const Animation = {
237
277
  map(animation, f) {
238
278
  return Animation.of({
239
279
  at: (t) => f(animation.at(t)),
240
- nextChange: (t) => animation.nextChange(t),
241
- lastChange: (t) => animation.lastChange(t),
280
+ nextChange: (t, moves) => animation.nextChange(t, moves),
281
+ lastChange: (t, moves) => animation.lastChange(t, moves),
242
282
  reduced: () => Animation.map(animation.reduced(), f),
243
283
  });
244
284
  },
@@ -255,8 +295,8 @@ export const Animation = {
255
295
  at: (t) => animations.map((animation) => animation.at(t)),
256
296
  // A fold, not a spread: a stagger over a table's rows is a list as long
257
297
  // as the table, past what an engine takes as arguments.
258
- nextChange: (t) => animations.reduce((soonest, animation) => Math.min(soonest, animation.nextChange(t)), Number.POSITIVE_INFINITY),
259
- lastChange: (t) => animations.reduce((latest, animation) => Math.max(latest, animation.lastChange(t)), Number.NEGATIVE_INFINITY),
298
+ nextChange: (t, moves) => animations.reduce((soonest, animation) => Math.min(soonest, animation.nextChange(t, moves)), Number.POSITIVE_INFINITY),
299
+ lastChange: (t, moves) => animations.reduce((latest, animation) => Math.max(latest, animation.lastChange(t, moves)), Number.NEGATIVE_INFINITY),
260
300
  reduced: () => Animation.all(animations.map((animation) => animation.reduced())),
261
301
  });
262
302
  },
@@ -275,8 +315,8 @@ export const Animation = {
275
315
  onBeat(animation, beat) {
276
316
  return Animation.of({
277
317
  at: (t) => animation.at(beat.lastChange(t)),
278
- nextChange(t) {
279
- const moved = animation.nextChange(beat.lastChange(t));
318
+ nextChange(t, moves) {
319
+ const moved = animation.nextChange(beat.lastChange(t), moves);
280
320
  if (moved === Number.POSITIVE_INFINITY)
281
321
  return moved;
282
322
  const end = beat.nextChange(t);
@@ -285,8 +325,13 @@ export const Animation = {
285
325
  return moved <= end ? end : beat.lastChange(moved) === moved ? moved : beat.nextChange(moved);
286
326
  },
287
327
  // The value held now began at this hold's start at the latest: an early
288
- // answer where the animation had not moved by it, which the contract allows.
289
- lastChange: (t) => beat.lastChange(t),
328
+ // answer where the animation had not moved by it, which the contract
329
+ // allows. One that always was never turned on any beat.
330
+ lastChange(t, moves) {
331
+ const began = beat.lastChange(t);
332
+ const moved = animation.lastChange(began, moves);
333
+ return moved === Number.NEGATIVE_INFINITY ? moved : began;
334
+ },
290
335
  // Reduced, nothing moves between beats, so there is nothing to sample on them.
291
336
  reduced: () => animation.reduced(),
292
337
  });
@@ -310,19 +355,19 @@ export const Animation = {
310
355
  const carried = (local) => (Number.isFinite(local) ? firstReaching(local + seconds, (x) => x - seconds >= local) : local);
311
356
  return Animation.of({
312
357
  at: (t) => (t - seconds >= 0 ? then.at(t - seconds) : first.at(t)),
313
- nextChange(t) {
358
+ nextChange(t, moves) {
314
359
  const into = t - seconds;
315
360
  if (into < 0)
316
- return Math.min(first.nextChange(t), seconds);
317
- return Math.max(t, carried(then.nextChange(into)));
361
+ return Math.min(first.nextChange(t, moves), seconds);
362
+ return Math.max(t, carried(then.nextChange(into, moves)));
318
363
  },
319
364
  // After the switch the value became what it is at the switch or when
320
365
  // `then`'s did since, whichever is later; before it, when `first`'s did.
321
- lastChange(t) {
366
+ lastChange(t, moves) {
322
367
  const into = t - seconds;
323
368
  if (into < 0)
324
- return first.lastChange(t);
325
- return Math.min(t, Math.max(seconds, carried(then.lastChange(into))));
369
+ return first.lastChange(t, moves);
370
+ return Math.min(t, Math.max(seconds, carried(then.lastChange(into, moves))));
326
371
  },
327
372
  reduced: () => Animation.sequence(first.reduced(), seconds, then.reduced()),
328
373
  });
@@ -360,14 +405,14 @@ export const Animation = {
360
405
  const { pass, into } = passAt("Animation.repeat", t, period);
361
406
  return nth(pass).at(into);
362
407
  },
363
- nextChange(t) {
408
+ nextChange(t, moves) {
364
409
  const { pass, into } = passAt("Animation.repeat", t, period);
365
- return Math.max(t, Math.min(turns.nextChange(t), carried(pass, turns.lastChange(t), nth(pass).nextChange(into))));
410
+ return Math.max(t, Math.min(turns.nextChange(t), carried(pass, turns.lastChange(t), nth(pass).nextChange(into, moves))));
366
411
  },
367
- lastChange(t) {
412
+ lastChange(t, moves) {
368
413
  const { pass, into } = passAt("Animation.repeat", t, period);
369
414
  const began = turns.lastChange(t);
370
- return Math.min(t, Math.max(began, carried(pass, began, nth(pass).lastChange(into))));
415
+ return Math.min(t, Math.max(began, carried(pass, began, nth(pass).lastChange(into, moves))));
371
416
  },
372
417
  reduced: () => Animation.repeat(period, (n) => nth(n).reduced()),
373
418
  });
@@ -21,18 +21,25 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
package/dist/core/ansi.js CHANGED
@@ -21,48 +21,78 @@
21
21
  *
22
22
  * Carriage returns are the other departure, and the same argument. Rich splits
23
23
  * lines with Python's `splitlines`, which also breaks at `\r`, so its `a\rb`
24
- * is two lines. Here a line ends only at `\n`, and `\r` returns to the first
25
- * column, so later text overwrites earlier text character by character:
26
- * `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
- * is still `done`. Columns are counted in characters, not cells, so a wide
28
- * character overwritten by a narrow one is where this and a terminal differ.
29
- * Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
24
+ * is two lines. Here a line ends at `\n` and the census's other line breaks, and
25
+ * `\r` returns to the first column, so later text overwrites earlier text cell by
26
+ * cell: `50%\r100%` is `100%`, `Downloading\rDone` is `Doneloading`, and `done\r`
27
+ * is still `done`. Columns are a terminal's cells: a wide character takes two, a
28
+ * combining mark none, a tab moves to the next stop over what is there, and a wide
29
+ * character or a tab half overwritten leaves blanks. Erase in line (`\x1b[K`) is honoured for the same reason, because a redraw
30
30
  * is usually `\r\x1b[K` and the old text must not show through a shorter new
31
31
  * one.
32
32
  *
33
+ * Backspace is the third. Rich drops it; here it is nroff's overstrike, the
34
+ * adjacent `x BS x` bold and `_ BS x` underline that `man` writes, and a bare one
35
+ * moves back a column, so `12%\b\b\b13%` is the redraw it looks like. The bytes
36
+ * alone cannot tell `1\b1` the redraw from `1\b1` the bold, and `less` reads them
37
+ * as overstrike with no other evidence, so this does.
38
+ *
33
39
  * Everything else follows Rich: malformed SGR parameters are skipped rather
34
- * than refused, and every other escape is dropped. It is not a terminal
35
- * emulator — any other cursor movement is dropped, not performed.
40
+ * than refused, and every other escape is dropped, with every control a terminal
41
+ * draws nothing for. It is not a terminal emulator — any other cursor movement is
42
+ * dropped, not performed.
36
43
  *
37
44
  * Tier 5 of `src/core/`: it builds `RichText`, so it sits above `text`, and
38
45
  * `RichText` cannot offer a `fromAnsi` static without importing upward.
39
46
  */
47
+ import { cellLen, graphemes } from "./cells.js";
40
48
  import { ColorSpec } from "./color.js";
49
+ import { controlClass } from "./control-codes.js";
41
50
  import { osc8Sequences } from "./osc8.js";
42
51
  import { ATTRIBUTE_NAMES, ATTRIBUTE_SGR, NULL_STYLE, Style } from "./style.js";
43
52
  import { RichText } from "./text.js";
53
+ const BOLD = new Style({ bold: true });
54
+ const UNDERLINED = new Style({ underline: true });
55
+ const BOLD_UNDERLINED = BOLD.add(UNDERLINED);
56
+ const STRING_ESCAPE = String.raw `\x1b[\]P_^X][^\n]*?(?:\x07|\x1b\\|\x9c|\n|$)`;
44
57
  /**
45
58
  * Every escape but OSC 8, which `osc8Sequences` reads before this runs. In
46
- * order: a carriage return; an SGR sequence (group 1, its parameters — a
47
- * private marker such as the `>` of `\x1b[>4;2m` makes it some other CSI);
48
- * erase in line (group 2, its mode); any other CSI sequence; any other string
49
- * escape — OSC, DCS, APC, PM, SOS — run to the terminators OSC 8 accepts or
50
- * cut off by the end of the line; any other escape, ECMA-48's intermediates
51
- * and one final byte (`\x1b(B`, `\x1b)0`, `\x1b#8`, `\x1bc`). Only the first
52
- * three become tokens.
59
+ * order: a carriage return; an SGR sequence (`sgr`, its parameters — a private
60
+ * marker such as the `>` of `\x1b[>4;2m` makes it some other CSI); erase in line
61
+ * (`mode`); any other CSI sequence; any other string escape — OSC, DCS, APC, PM,
62
+ * SOS — run to the terminators OSC 8 accepts or cut off by the end of the line;
63
+ * any other escape, ECMA-48's intermediates and one final byte (`\x1b(B`,
64
+ * `\x1b)0`, `\x1b#8`, `\x1bc`); a character struck over itself (`struck`), over an
65
+ * underscore first or not (`underscore`); a character struck over an underscore
66
+ * alone (`underlined`); a backspace; and every control a line may not hold or a
67
+ * terminal draws nothing for. The silent ones are the two "any other" escapes and
68
+ * that last class.
53
69
  */
54
- const escapes = () => /\r|\x1b\[([0-9;:]*)m|\x1b\[([012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[\]P_^X][\s\S]*?(?:\x07|\x1b\\|\x9c|$)|\x1b[ -/]*[0-~]/g;
70
+ const ESCAPES = [
71
+ String.raw `\r|\x1b\[(?<sgr>[0-9;:]*)m|\x1b\[(?<mode>[012]?)K|\x1b\[[0-?]*[ -/]*[@-~]|${STRING_ESCAPE}|\x1b[ -/]*[0-~]`,
72
+ String.raw `(?<underscore>_\x08)?(?<struck>\P{Cc})(?:\x08\k<struck>)+|_\x08(?<underlined>\P{Cc})|\x08`,
73
+ controlClass((disposition) => disposition !== "keep" && disposition !== "withheld"),
74
+ ].join("|");
75
+ const escapes = () => new RegExp(ESCAPES, "gu");
76
+ // A string escape goes first so a line break in its payload stays in the payload.
77
+ const lineBreaks = () => new RegExp(`${STRING_ESCAPE}|${controlClass((disposition) => disposition === "line-break")}`, "gu");
55
78
  function* escapeTokens(bytes) {
56
79
  let at = 0;
57
80
  for (const match of bytes.matchAll(escapes())) {
81
+ const { sgr, mode, underscore, struck, underlined } = match.groups;
58
82
  if (match.index > at)
59
83
  yield { kind: "text", text: bytes.slice(at, match.index) };
60
84
  if (match[0] === "\r")
61
85
  yield { kind: "return" };
62
- if (match[1] !== undefined)
63
- yield { kind: "sgr", params: match[1] };
64
- if (match[2] !== undefined)
65
- yield { kind: "erase", mode: (match[2] || "0") };
86
+ if (sgr !== undefined)
87
+ yield { kind: "sgr", params: sgr };
88
+ if (mode !== undefined)
89
+ yield { kind: "erase", mode: (mode || "0") };
90
+ if (struck !== undefined)
91
+ yield { kind: "overstrike", character: struck, emphasis: underscore === undefined ? BOLD : BOLD_UNDERLINED };
92
+ if (underlined !== undefined)
93
+ yield { kind: "overstrike", character: underlined, emphasis: UNDERLINED };
94
+ if (match[0] === "\b")
95
+ yield { kind: "backspace" };
66
96
  at = match.index + match[0].length;
67
97
  }
68
98
  if (at < bytes.length)
@@ -153,11 +183,52 @@ function applySgr(style, params) {
153
183
  result = (SGR_OPS.get(code) ?? identity)(result, codes);
154
184
  return result;
155
185
  }
186
+ const ORIGIN = ["", NULL_STYLE];
156
187
  const BLANK = [" ", NULL_STYLE];
188
+ const TAB_STOP = 8;
189
+ /** The column holding the cluster drawn across `column`. */
190
+ function leadOf(cells, column) {
191
+ let at = column;
192
+ while (at > 0 && cells[at][0] === "")
193
+ at -= 1;
194
+ return at;
195
+ }
196
+ /** A cluster across the edge before `column` goes blank, as on a terminal. */
197
+ function splitWide(cells, column) {
198
+ if (cells[column]?.[0] !== "")
199
+ return;
200
+ const lead = leadOf(cells, column);
201
+ for (let at = lead; at === lead || cells[at]?.[0] === ""; at += 1)
202
+ cells[at] = BLANK;
203
+ }
204
+ /** `cluster` drawn at `column`, and the columns the cursor moves. */
205
+ function draw(cells, column, cluster, style) {
206
+ const width = cluster === "\t" ? TAB_STOP - ((column - 1) % TAB_STOP) : cellLen(cluster);
207
+ if (width === 0) {
208
+ const lead = leadOf(cells, column - 1);
209
+ cells[lead] = [cells[lead][0] + cluster, cells[lead][1]];
210
+ return 0;
211
+ }
212
+ if (cluster === "\t" && column < cells.length) {
213
+ while (cells.length < column + width)
214
+ cells.push(BLANK);
215
+ return width;
216
+ }
217
+ splitWide(cells, column);
218
+ splitWide(cells, column + width);
219
+ cells[column] = [cells[0][0] + cluster, style];
220
+ cells[0] = ORIGIN;
221
+ for (let at = 1; at < width; at += 1)
222
+ cells[column + at] = ["", style];
223
+ return width;
224
+ }
157
225
  /** Erase in line: the cells before the cursor go blank, the ones from it on go. */
158
226
  function erase(cells, column, mode) {
227
+ splitWide(cells, column);
228
+ splitWide(cells, column + 1);
229
+ cells[0] = ORIGIN;
159
230
  if (mode !== "0")
160
- cells.fill(BLANK, 0, Math.min(column + 1, cells.length));
231
+ cells.fill(BLANK, 1, Math.min(column + 1, cells.length));
161
232
  if (mode !== "1")
162
233
  cells.length = Math.min(column, cells.length);
163
234
  }
@@ -173,25 +244,32 @@ export class AnsiDecoder {
173
244
  link = undefined;
174
245
  /** One `RichText` per line of `ansi`; a final newline ends a line rather than starting one. */
175
246
  decode(ansi) {
176
- return (ansi.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
247
+ const lined = ansi.replace(lineBreaks(), (found) => (found.startsWith("\x1b") ? found : "\n"));
248
+ return (lined.match(/[^\n]*\n|[^\n]+/g) ?? []).map((line) => this.decodeLine(line.replace(/\r?\n$/, "")));
177
249
  }
178
250
  /**
179
251
  * One line of output, holding no `\n`, written the way a terminal writes
180
252
  * it: `\r` returns to the first column, and text overwrites from there.
181
253
  */
182
254
  decodeLine(line) {
183
- const cells = [];
184
- let column = 0;
255
+ const cells = [ORIGIN];
256
+ let column = 1;
185
257
  for (const token of tokens(line)) {
186
258
  switch (token.kind) {
187
259
  case "text": {
188
260
  const style = this.sgr.withLink(this.link);
189
- for (const char of token.text)
190
- cells[column++] = [char, style];
261
+ for (const cluster of graphemes(token.text))
262
+ column += draw(cells, column, cluster, style);
191
263
  break;
192
264
  }
265
+ case "overstrike":
266
+ column += draw(cells, column, token.character, this.sgr.add(token.emphasis).withLink(this.link));
267
+ break;
193
268
  case "return":
194
- column = 0;
269
+ column = 1;
270
+ break;
271
+ case "backspace":
272
+ column = Math.max(1, column - 1);
195
273
  break;
196
274
  case "erase":
197
275
  erase(cells, column, token.mode);
@@ -205,7 +283,7 @@ export class AnsiDecoder {
205
283
  }
206
284
  }
207
285
  const runs = [];
208
- for (const [char, style] of cells) {
286
+ for (const [char, style] of cells.filter(([char]) => char !== "")) {
209
287
  const last = runs.at(-1);
210
288
  if (last?.[1].equals(style))
211
289
  last[0] += char;