@promptctl/rich-js 0.27.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 (110) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +36 -17
  4. package/dist/core/animation.js +61 -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 +6 -7
  18. package/dist/core/color.js +11 -13
  19. package/dist/core/console.d.ts +18 -1
  20. package/dist/core/console.js +24 -5
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/env.d.ts +6 -0
  24. package/dist/core/env.js +9 -1
  25. package/dist/core/export-html.js +4 -3
  26. package/dist/core/export-lines.d.ts +2 -0
  27. package/dist/core/export-lines.js +7 -3
  28. package/dist/core/export-svg.js +6 -5
  29. package/dist/core/json.js +9 -2
  30. package/dist/core/markup.js +4 -3
  31. package/dist/core/orient.d.ts +68 -0
  32. package/dist/core/orient.js +224 -0
  33. package/dist/core/osc8.d.ts +12 -5
  34. package/dist/core/osc8.js +14 -7
  35. package/dist/core/pretty.js +22 -18
  36. package/dist/core/protocol.d.ts +27 -6
  37. package/dist/core/protocol.js +14 -6
  38. package/dist/core/render.d.ts +8 -3
  39. package/dist/core/render.js +18 -5
  40. package/dist/core/seal.d.ts +26 -0
  41. package/dist/core/seal.js +75 -0
  42. package/dist/core/segment.d.ts +9 -1
  43. package/dist/core/segment.js +11 -1
  44. package/dist/core/style.d.ts +29 -1
  45. package/dist/core/style.js +41 -5
  46. package/dist/core/text-order.d.ts +51 -0
  47. package/dist/core/text-order.js +50 -0
  48. package/dist/core/text.d.ts +10 -0
  49. package/dist/core/text.js +133 -87
  50. package/dist/core/timeline.js +6 -6
  51. package/dist/core/view.d.ts +1 -1
  52. package/dist/core/view.js +5 -4
  53. package/dist/core/wrap.d.ts +24 -6
  54. package/dist/core/wrap.js +82 -16
  55. package/dist/host/app.d.ts +44 -3
  56. package/dist/host/app.js +111 -6
  57. package/dist/host/index.d.ts +3 -2
  58. package/dist/host/index.js +1 -0
  59. package/dist/host/input.d.ts +35 -2
  60. package/dist/host/input.js +88 -4
  61. package/dist/host/terminal-colors.d.ts +18 -0
  62. package/dist/host/terminal-colors.js +46 -0
  63. package/dist/host/terminal-host.d.ts +2 -1
  64. package/dist/index.d.ts +5 -4
  65. package/dist/index.js +2 -2
  66. package/dist/renderables/change.d.ts +1 -3
  67. package/dist/renderables/change.js +4 -6
  68. package/dist/renderables/effect.d.ts +60 -59
  69. package/dist/renderables/effect.js +91 -81
  70. package/dist/renderables/effects.d.ts +37 -8
  71. package/dist/renderables/effects.js +185 -52
  72. package/dist/renderables/frames.d.ts +1 -10
  73. package/dist/renderables/frames.js +1 -16
  74. package/dist/renderables/glyph-effect.d.ts +12 -13
  75. package/dist/renderables/glyph-effect.js +34 -32
  76. package/dist/renderables/glyph-effects.d.ts +27 -8
  77. package/dist/renderables/glyph-effects.js +75 -7
  78. package/dist/renderables/lifecycle.d.ts +5 -7
  79. package/dist/renderables/lifecycle.js +6 -7
  80. package/dist/renderables/markdown.js +4 -2
  81. package/dist/renderables/panel.js +6 -4
  82. package/dist/renderables/progressBar.d.ts +3 -3
  83. package/dist/renderables/progressBar.js +16 -13
  84. package/dist/renderables/syntax.js +7 -2
  85. package/dist/renderables/table.js +2 -1
  86. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  87. package/dist/template-bindings/animation-funcs.js +115 -32
  88. package/dist/template-bindings/index.d.ts +7 -5
  89. package/dist/template-bindings/index.js +8 -5
  90. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  91. package/dist/template-bindings/palette-funcs.js +1 -1
  92. package/dist/themes/buildPalette.d.ts +3 -2
  93. package/dist/themes/buildPalette.js +2 -2
  94. package/dist/themes/data/index.js +1 -4
  95. package/dist/themes/data/textual-ansi.js +0 -143
  96. package/dist/themes/ramp.d.ts +1 -1
  97. package/dist/themes/ramp.js +1 -1
  98. package/dist/themes/registry.d.ts +6 -8
  99. package/dist/themes/registry.js +2 -4
  100. package/dist/themes/terminalThemes.d.ts +4 -11
  101. package/dist/themes/terminalThemes.js +31 -47
  102. package/dist/widgets/button.d.ts +1 -1
  103. package/dist/widgets/button.js +6 -6
  104. package/dist/widgets/checkbox.js +4 -4
  105. package/dist/widgets/dropdown.d.ts +1 -1
  106. package/dist/widgets/dropdown.js +7 -5
  107. package/dist/widgets/themed-widget.js +3 -3
  108. package/dist/widgets/toggle.d.ts +1 -1
  109. package/dist/widgets/toggle.js +5 -5
  110. package/package.json +5 -4
@@ -51,44 +51,40 @@
51
51
  * Positions are the child's own: row and column count from the top-left of
52
52
  * what the child drew.
53
53
  */
54
- import { Animation, frameTimeOf, holdOf, shownAt, stampFrame } from "../core/animation.js";
54
+ import { Animation, placedRender, shownAt } from "../core/animation.js";
55
55
  import { cellLen, graphemes } from "../core/cells.js";
56
56
  import { Measurement } from "../core/measure.js";
57
57
  import { render } from "../core/protocol.js";
58
58
  import { Segment } from "../core/segment.js";
59
59
  import { finiteTime } from "../core/easing.js";
60
- import { stripControlChars } from "../core/text.js";
61
- import { cellSeed, walkGlyphs } from "./effect.js";
60
+ import { sanitized } from "../core/text.js";
61
+ import { cellSeed, holdOver, walkGlyphs } from "./effect.js";
62
62
  import { pattern } from "../core/pattern.js";
63
63
  /**
64
64
  * A `GlyphEffected` at `t`, or an element through a glyph effect at a time an
65
65
  * animation's `at` was handed: `renderable` with its cells' glyphs passed
66
- * through `effect` at `t`, saying nothing of when that changes, as a
67
- * `Recolored` is (`./effect.ts`). [LAW:one-type-per-behavior]
66
+ * through `effect` at `t`, telling the hold it drew in as a `Recolored` does
67
+ * (`RenderOptions.onMoved`, `./effect.ts`). [LAW:one-type-per-behavior]
68
68
  */
69
69
  export class Reglyphed {
70
70
  renderable;
71
71
  effect;
72
- t;
72
+ _t;
73
73
  key;
74
74
  constructor(renderable, effect, options) {
75
75
  this.renderable = renderable;
76
76
  this.effect = effect;
77
- this.t = finiteTime("GlyphEffected", options.t);
77
+ this._t = finiteTime("GlyphEffected", options.t);
78
78
  this.key = options.key;
79
79
  }
80
80
  render(options) {
81
- return this.moved(options).segments;
82
- }
83
- /** The frame the effect draws at `t`, and the hold it says that is in. */
84
- moved(options) {
85
81
  const segments = [...render(this.renderable, options)];
86
82
  const { placed, cells } = placeGlyphs(segments, this.key);
87
83
  const animation = this.effect({ cells, asciiOnly: options.asciiOnly === true });
88
- const frame = shownAt(animation, this.t, options);
84
+ const frame = shownAt(animation, this._t, options);
89
85
  const shown = showGlyphs(placed, frame.value);
90
- const changed = segments.map((segment, i) => shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style));
91
- return { segments: changed, hold: holdOf(frame, this.t) };
86
+ options.onMoved?.(this._t, this.key, holdOver(cells.length > 0, frame, this._t));
87
+ return segments.map((segment, i) => (shown[i].every((text, j) => text === placed[i][j].glyph) ? segment : new Segment(shown[i].join(""), segment.style)));
92
88
  }
93
89
  measure(options) {
94
90
  return Measurement.get(options, this.renderable);
@@ -96,9 +92,10 @@ export class Reglyphed {
96
92
  }
97
93
  /**
98
94
  * `renderable` under a glyph effect over time, an `Animation<Renderable>`
99
- * as an `Effected` is (`./effect.ts`): its own answer is `t`, and placed,
100
- * its cells carry the glyph effect's exact hold at the frame's time and the
101
- * output's `asciiOnly`, under whatever the element already said.
95
+ * as an `Effected` is (`./effect.ts`): with nothing drawn its answer is `t`,
96
+ * and placed, on its own or as a part of any combinator, it says the glyph
97
+ * effect's exact hold at that time and the output's `asciiOnly`, which the
98
+ * frame's drawing told (`Moves`).
102
99
  */
103
100
  export class GlyphEffected {
104
101
  renderable;
@@ -113,11 +110,11 @@ export class GlyphEffected {
113
110
  at(t) {
114
111
  return new Reglyphed(this.renderable, this.effect, { t, key: this.key });
115
112
  }
116
- nextChange(t) {
117
- return t;
113
+ nextChange(t, moves) {
114
+ return moves?.(t, this.key)?.nextChange ?? t;
118
115
  }
119
- lastChange(t) {
120
- return t;
116
+ lastChange(t, moves) {
117
+ return moves?.(t, this.key)?.lastChange ?? t;
121
118
  }
122
119
  /** The element under the glyph effect with its motion taken out. */
123
120
  reduced() {
@@ -126,8 +123,7 @@ export class GlyphEffected {
126
123
  }
127
124
  /** Drawn at the frame's time, its cells stamped with the glyph effect's exact hold there. */
128
125
  render(options) {
129
- const { segments, hold } = this.at(frameTimeOf(options)).moved(options);
130
- return stampFrame(segments, hold);
126
+ return placedRender(this, options);
131
127
  }
132
128
  measure(options) {
133
129
  return Measurement.get(options, this.renderable);
@@ -161,7 +157,7 @@ function showGlyphs(placed, frame) {
161
157
  * its every style and span, each moved to where its glyphs now sit, and a
162
158
  * frame that changes nothing hands the text back as it was. What it says of
163
159
  * when it changes is the effect's, unstamped: whoever draws it at a time
164
- * stamps it (`textFrameAt`).
160
+ * stamps it (`placed`), or marks it for the engine to (the template `glyphs`).
165
161
  */
166
162
  export function reglyphText(text, effect, options) {
167
163
  const { placed, cells } = placeGlyphs([new Segment(text.plain)], options.key);
@@ -193,7 +189,7 @@ function rewritten(text, glyphs, shown) {
193
189
  result.stylize(span.style, moved(span.start), moved(span.end));
194
190
  return result;
195
191
  }
196
- /** Line breaks and tabs, which `stripControlChars` leaves for a text's own lines and which no cell may hold. */
192
+ /** Line breaks and tabs, which `sanitized` leaves for a text's own lines and which no cell may hold. */
197
193
  const BREAKS = pattern(/[\n\t]/);
198
194
  /**
199
195
  * What `frame` shows in `cell`, held to the contract: as many cells wide as
@@ -205,7 +201,7 @@ function shows(frame, cell) {
205
201
  const text = frame(cell);
206
202
  if (text === cell.glyph)
207
203
  return text;
208
- if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || stripControlChars(text) !== text) {
204
+ if (cellLen(text) !== cellLen(cell.glyph) || BREAKS.test(text) || sanitized(text) !== text) {
209
205
  throw new RangeError(`a glyph effect turned ${JSON.stringify(cell.glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}: ` +
210
206
  `it must be ${cellLen(cell.glyph)} cell(s) wide, with no line break or control character`);
211
207
  }
@@ -218,18 +214,24 @@ function shows(frame, cell) {
218
214
  * wide as its glyph is `shows`'s check; this is the half no one cell can
219
215
  * see — a trailing zero-width joiner that fuses with the next emoji, or a
220
216
  * leading combining mark that draws on the glyph before it, is right alone
221
- * and wrong beside its neighbour. Only a changed text is asked, so a child
222
- * that splits a cluster across its own segments is drawn as it drew it.
217
+ * and wrong beside its neighbour. Only a join the effect made is asked, seam
218
+ * by seam, so a child that splits a cluster across its own segments is drawn
219
+ * as it drew it, whatever becomes of the glyph its mark sits on.
223
220
  */
224
221
  function standsApart(placed, shown) {
225
- // A line break is no cell, and nothing joins across it.
226
- const beside = (j) => (placed[j]?.cell === undefined ? "" : shown[j]);
222
+ const fuse = (left, right) => graphemes(left + right).length < graphemes(left).length + graphemes(right).length;
223
+ // Whether the text at `i` fuses across its left seam and across its right.
224
+ const seams = (texts, i) => {
225
+ // A line break is no cell, and nothing joins across it.
226
+ const beside = (j) => (placed[j]?.cell === undefined ? "" : texts(j));
227
+ const [before, text, after] = [beside(i - 1), texts(i), beside(i + 1)];
228
+ return [fuse(before, text + after), fuse(before + text, after)];
229
+ };
227
230
  shown.forEach((text, i) => {
228
231
  const { glyph, cell } = placed[i];
229
232
  if (cell === undefined || text === glyph)
230
233
  return;
231
- const [before, after] = [beside(i - 1), beside(i + 1)];
232
- if (graphemes(before + text + after).length !== graphemes(before).length + graphemes(text).length + graphemes(after).length) {
234
+ if (seams((j) => shown[j], i).some((fused, seam) => fused && !seams((j) => placed[j].glyph, i)[seam])) {
233
235
  throw new RangeError(`a glyph effect turned ${JSON.stringify(glyph)} at row ${cell.row}, column ${cell.col} into ${JSON.stringify(text)}, ` +
234
236
  `which joins the cell beside it into one character`);
235
237
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * The glyph effect catalogue: a typewriter and a scramble, which run once and
3
3
  * are done, each with its way out for an element leaving (`backspace`,
4
- * `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
5
- * long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
6
- * (`./glyph-effect.ts`), built as the colour catalogue's are
7
- * (`./effects.ts`): one function each over the one shape, made by name, one
8
- * way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
4
+ * `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
5
+ * which loop for as long as they are drawn. Each is a `GlyphEffect` for
6
+ * `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
7
+ * (`./effects.ts`): one function each over the one shape, at the defaults in
8
+ * `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
9
+ * `glyphs` takes.
9
10
  *
10
11
  * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
12
  * another, so every loop here moves in steps, on a `beat`, and says when its
@@ -90,11 +91,25 @@ export interface Particles {
90
91
  * row a step, and one that holds still draws a fresh field each life.
91
92
  */
92
93
  export declare function particles(spec: Particles, z: number): GlyphEffect;
93
- /** The names of the glyph effects that loop. */
94
+ /** How ants march: a step, in seconds, for the gaps to move one cell, and the most cells a gap may be from the next. */
95
+ export interface MarchingAnts {
96
+ readonly step: number;
97
+ readonly every: number;
98
+ }
99
+ /**
100
+ * Marching ants: gaps blanked round each box the element draws, spaced as
101
+ * evenly as the ring allows and never more than `every` cells apart, walking
102
+ * one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
103
+ * does not divide closes with no seam at its corner. The ring is whatever the box
104
+ * is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
105
+ * they keep the count, but an element with no ink on any ring never changes.
106
+ */
107
+ export declare function marchingAnts({ step, every }: MarchingAnts): GlyphEffect;
108
+ /** The names a template's `glyphs` takes for an effect that loops. */
94
109
  export type GlyphLoopName = "wave" | "rain" | "stars";
95
- /** The names of the glyph effects that run once. */
110
+ /** The names a template's `glyphs` takes for an effect that runs once. */
96
111
  export type GlyphTransitionName = "typewriter" | "scramble" | "backspace" | "scrambleOut";
97
- /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
112
+ /** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
98
113
  export declare const GLYPH_DEFAULTS: {
99
114
  readonly typewriter: {
100
115
  readonly seconds: 2;
@@ -132,6 +147,10 @@ export declare const GLYPH_DEFAULTS: {
132
147
  readonly drift: 0;
133
148
  readonly life: 1;
134
149
  };
150
+ readonly march: {
151
+ readonly step: 0.15;
152
+ readonly every: 3;
153
+ };
135
154
  };
136
155
  /**
137
156
  * Every glyph effect in the catalogue by name, each made the one way: for an
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * The glyph effect catalogue: a typewriter and a scramble, which run once and
3
3
  * are done, each with its way out for an element leaving (`backspace`,
4
- * `scrambleOut`), and a wave of case changes, rain and stars, which loop for as
5
- * long as they are drawn. Each is a `GlyphEffect` for `GlyphEffected`
6
- * (`./glyph-effect.ts`), built as the colour catalogue's are
7
- * (`./effects.ts`): one function each over the one shape, made by name, one
8
- * way, at the defaults in `GLYPH_DEFAULTS` (`GLYPH_EFFECT_CATALOGUE`).
4
+ * `scrambleOut`), and a wave of case changes, rain, stars and marching ants,
5
+ * which loop for as long as they are drawn. Each is a `GlyphEffect` for
6
+ * `GlyphEffected` (`./glyph-effect.ts`), built as the colour catalogue's are
7
+ * (`./effects.ts`): one function each over the one shape, at the defaults in
8
+ * `GLYPH_DEFAULTS`. `GLYPH_EFFECT_CATALOGUE` makes by name those a template's
9
+ * `glyphs` takes.
9
10
  *
10
11
  * Where a colour loop glides, a glyph cannot: a cell shows one glyph or
11
12
  * another, so every loop here moves in steps, on a `beat`, and says when its
@@ -50,6 +51,10 @@ function text(cells) {
50
51
  }
51
52
  return { length: place.size, place };
52
53
  }
54
+ /** `n` wrapped into [0, `size`), for a count a timeline may run backwards past 0. */
55
+ function wrapped(n, size) {
56
+ return ((n % size) + size) % size;
57
+ }
53
58
  /** Of `n` cells, how many have arrived at eased progress `p`, an ease that overshoots held to the cells there are. */
54
59
  function arrived(n, p) {
55
60
  return Math.min(n, Math.max(0, Math.floor(n * p)));
@@ -200,7 +205,7 @@ export function caseWave({ step, width }, z) {
200
205
  const start = Math.floor(hash(z, 2.3) * span);
201
206
  return steps((s) => {
202
207
  // The column just past the band's right edge; it enters at the left.
203
- const lead = (((s + start) % span) + span) % span;
208
+ const lead = wrapped(s + start, span);
204
209
  // A wide glyph is in the band if any of its columns is.
205
210
  return (cell) => (cell.col < lead && cell.col + cellLen(cell.glyph) > lead - width ? swapCase(cell.glyph) : cell.glyph);
206
211
  }, AS_WRITTEN);
@@ -250,7 +255,69 @@ export function particles(spec, z) {
250
255
  }, AS_WRITTEN);
251
256
  };
252
257
  }
253
- /** Each catalogue glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
258
+ /**
259
+ * Where each cell stands on the ring of the box frame that drew it, by
260
+ * `index`. A ring is the
261
+ * perimeter of the rectangle a frame's cells cover, read in the element's own
262
+ * rows and columns, so it is true wherever the box sits among them. A rule or
263
+ * divider inside that rectangle, as a table draws, is frame and no part of the
264
+ * ring, and a title set into the frame is no frame but keeps its places.
265
+ */
266
+ function ringPlaces(cells) {
267
+ const frames = new Map();
268
+ for (const cell of cells) {
269
+ if (cell.boxFrame === undefined)
270
+ continue;
271
+ const ring = frames.get(cell.boxFrame);
272
+ if (ring === undefined)
273
+ frames.set(cell.boxFrame, [cell]);
274
+ else
275
+ ring.push(cell);
276
+ }
277
+ const places = new Map();
278
+ for (const ring of frames.values()) {
279
+ // Cells come in reading order, so the rows are the ring's two ends.
280
+ const [top, down] = [ring[0].row, ring[ring.length - 1].row - ring[0].row];
281
+ const left = ring.reduce((most, { col }) => Math.min(most, col), Infinity);
282
+ const across = ring.reduce((most, { col }) => Math.max(most, col), -Infinity) - left;
283
+ const size = Math.max(1, 2 * (across + down));
284
+ for (const cell of ring) {
285
+ const [y, x] = [cell.row - top, cell.col - left];
286
+ const place = y === 0 ? x : x === across ? across + y : y === down ? 2 * across + down - x : x === 0 ? 2 * (across + down) - y : undefined;
287
+ if (place !== undefined)
288
+ places.set(cell.index, { place, cells: size });
289
+ }
290
+ }
291
+ return places;
292
+ }
293
+ /**
294
+ * Marching ants: gaps blanked round each box the element draws, spaced as
295
+ * evenly as the ring allows and never more than `every` cells apart, walking
296
+ * one cell clockwise a `step`. The whole pattern turns, so a ring that `every`
297
+ * does not divide closes with no seam at its corner. The ring is whatever the box
298
+ * is (`Box.frame`), at any width and beside a title. A ring may draw blanks and
299
+ * they keep the count, but an element with no ink on any ring never changes.
300
+ */
301
+ export function marchingAnts({ step, every }) {
302
+ positiveSeconds("marchingAnts step", step);
303
+ if (!(Number.isInteger(every) && every > 1))
304
+ throw new RangeError(`marchingAnts every must be a whole number of cells above 1, got ${every}`);
305
+ const steps = stepped(step);
306
+ return ({ cells }) => {
307
+ const places = ringPlaces(cells);
308
+ if (!cells.some((cell) => places.has(cell.index) && inked(cell.glyph)))
309
+ return Animation.constant(AS_WRITTEN);
310
+ return steps((s) => (cell) => {
311
+ const at = places.get(cell.index);
312
+ if (at === undefined)
313
+ return cell.glyph;
314
+ // Bresenham's line over the ring: of `cells` places, the `gaps` where a running count crosses a whole number.
315
+ const gaps = Math.ceil(at.cells / every);
316
+ return (wrapped(at.place - s, at.cells) * gaps) % at.cells < gaps ? blank(cell.glyph) : cell.glyph;
317
+ }, AS_WRITTEN);
318
+ };
319
+ }
320
+ /** Each glyph effect's defaults, as first drawn in `examples/glyph-effects`. */
254
321
  export const GLYPH_DEFAULTS = {
255
322
  typewriter: { seconds: 2, ease: EASES.linear },
256
323
  scramble: { seconds: 2.5, ease: EASES["ease-in"] },
@@ -259,6 +326,7 @@ export const GLYPH_DEFAULTS = {
259
326
  wave: { step: 0.06, width: 3 },
260
327
  rain: { glyphs: "│╎", ascii: "|:", density: 0.06, step: 0.08, drift: 1, life: Number.POSITIVE_INFINITY },
261
328
  stars: { glyphs: "·+*", ascii: ".+*", density: 0.04, step: 1.5, drift: 0, life: 1 },
329
+ march: { step: 0.15, every: 3 },
262
330
  };
263
331
  /**
264
332
  * Every glyph effect in the catalogue by name, each made the one way: for an
@@ -29,9 +29,8 @@
29
29
  *
30
30
  * A half draws the element through a colour transition (`throughColors`, a
31
31
  * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
32
- * transition to the one wrapper that draws it, and to what that wrapper
33
- * needs — a theme for colours, nothing for glyphs — so nothing here asks which
34
- * kind a half is, and a line can type itself in and dissolve out. A half
32
+ * transition to the one wrapper that draws it, so nothing here asks which kind
33
+ * a half is, and a line can type itself in and dissolve out. A half
35
34
  * written by hand is any function of that shape — an arrival that slides the
36
35
  * element in, which no effect draws. An exit is a half that ends with nothing
37
36
  * drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
@@ -43,7 +42,6 @@
43
42
  * backwards — it draws nothing too, but is not gone.
44
43
  */
45
44
  import { Animation } from "../core/animation.js";
46
- import type { TerminalTheme } from "../core/color.js";
47
45
  import type { Renderable } from "../core/protocol.js";
48
46
  import type { Transition } from "./effects.js";
49
47
  import type { GlyphTransition } from "./glyph-effects.js";
@@ -59,13 +57,13 @@ import type { GlyphTransition } from "./glyph-effects.js";
59
57
  * `throughColors` and `throughGlyphs` lend one the transition's schedule.
60
58
  */
61
59
  export type Half = (element: Renderable, key: string) => Animation<Renderable>;
62
- /** `transition`'s colours drawn over an element on `theme`, whose ground it arrives from and leaves to. */
63
- export declare function throughColors(transition: Transition, theme: TerminalTheme): Half;
60
+ /** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
61
+ export declare function throughColors(transition: Transition): Half;
64
62
  /** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
65
63
  export declare function throughGlyphs(transition: GlyphTransition): Half;
66
64
  /** How an element arrives and leaves, and what it is named for their seeds. */
67
65
  export interface LifecycleOptions {
68
- /** The half it arrives through — `throughColors(fadeIn(curve, z), theme)`. */
66
+ /** The half it arrives through — `throughColors(fadeIn(curve, z))`. */
69
67
  readonly enter: Half;
70
68
  /** The half it leaves through — `throughGlyphs(scrambleOut(timing, z))`. */
71
69
  readonly exit: Half;
@@ -29,9 +29,8 @@
29
29
  *
30
30
  * A half draws the element through a colour transition (`throughColors`, a
31
31
  * `fadeIn`) or a glyph one (`throughGlyphs`, a `typewriter`): each binds its
32
- * transition to the one wrapper that draws it, and to what that wrapper
33
- * needs — a theme for colours, nothing for glyphs — so nothing here asks which
34
- * kind a half is, and a line can type itself in and dissolve out. A half
32
+ * transition to the one wrapper that draws it, so nothing here asks which kind
33
+ * a half is, and a line can type itself in and dissolve out. A half
35
34
  * written by hand is any function of that shape — an arrival that slides the
36
35
  * element in, which no effect draws. An exit is a half that ends with nothing
37
36
  * drawn — `dissolveOut`, `backspace`, `scrambleOut` — so the element has
@@ -47,9 +46,9 @@ import { firstReaching } from "../core/easing.js";
47
46
  import { Timeline } from "../core/timeline.js";
48
47
  import { Effected } from "./effect.js";
49
48
  import { GlyphEffected } from "./glyph-effect.js";
50
- /** `transition`'s colours drawn over an element on `theme`, whose ground it arrives from and leaves to. */
51
- export function throughColors(transition, theme) {
52
- return (element, key) => scheduled(new Effected(element, transition.effect, { key, theme }), transition.schedule);
49
+ /** `transition`'s colours drawn over an element, arriving from and leaving to the ground of the terminal the frame is drawn on. */
50
+ export function throughColors(transition) {
51
+ return (element, key) => scheduled(new Effected(element, transition.effect, { key }), transition.schedule);
53
52
  }
54
53
  /** `transition`'s glyphs drawn over an element, at every depth, with no colour too. */
55
54
  export function throughGlyphs(transition) {
@@ -107,7 +106,7 @@ function doneAt(half, start) {
107
106
  */
108
107
  function ending(half, role, key) {
109
108
  if (doneAt(half, 0) === Number.POSITIVE_INFINITY) {
110
- throw new RangeError(`lifecycle "${key}": the ${role} half never ends: its animation never says it has stopped changing (Animation.done). An Effected or GlyphEffected on its own does not; throughColors(transition, theme) and throughGlyphs(transition) lend one the transition's schedule`);
109
+ throw new RangeError(`lifecycle "${key}": the ${role} half never ends: its animation never says it has stopped changing (Animation.done). An Effected or GlyphEffected on its own does not; throughColors(transition) and throughGlyphs(transition) lend one the transition's schedule`);
111
110
  }
112
111
  return half;
113
112
  }
@@ -8,6 +8,7 @@
8
8
  * followed: HTML, a block of it or a tag inline, is drawn as Rich draws it,
9
9
  * not at all.
10
10
  */
11
+ import { withoutExplicit } from "../core/bidi.js";
11
12
  import { cellCount, cellLen } from "../core/cells.js";
12
13
  import { Segment } from "../core/segment.js";
13
14
  import { Style } from "../core/style.js";
@@ -107,7 +108,8 @@ function appendInline(result, inlines, settings, options) {
107
108
  };
108
109
  const write = (text, styles) => {
109
110
  const start = result.length;
110
- result.append(text);
111
+ // Each inline node is its own content, so what it holds open ends with it.
112
+ result.append(new RichText(text));
111
113
  paint(start, styles);
112
114
  };
113
115
  const enter = (styles) => {
@@ -156,7 +158,7 @@ function appendInline(result, inlines, settings, options) {
156
158
  write("\n");
157
159
  break;
158
160
  case "code":
159
- write(inline.code, [groundedStyle(options, resolveStyle(options, settings.inlineCodeStyle))]);
161
+ write(withoutExplicit(inline.code), [groundedStyle(options, resolveStyle(options, settings.inlineCodeStyle))]);
160
162
  break;
161
163
  case "html":
162
164
  kbd(inline.content);
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Panel — a bordered box that wraps content, with optional title and subtitle.
3
3
  */
4
+ import { labelled } from "../core/orient.js";
4
5
  import { cellLen, setCellSize, asCellCol, cellCount } from "../core/cells.js";
5
6
  import { Segment } from "../core/segment.js";
6
7
  import { NULL_STYLE } from "../core/style.js";
@@ -42,10 +43,11 @@ function borderTitle(label, options) {
42
43
  * its own laid over the border's, the one rule for "what colour is the title
43
44
  * text in". Laid over rather than in place of it, as Rich's `stylize_before`
44
45
  * does, because the border's style carries the panel's own: a title that
45
- * replaced it cut a hole in the panel's background on the top row.
46
+ * replaced it cut a hole in the panel's background on the top row. The text
47
+ * is set into the frame and is not of it.
46
48
  */
47
49
  function borderTextStyle(border, own) {
48
- return own.isNull ? border : (border ?? NULL_STYLE).add(own);
50
+ return border.add(own).withBoxFrame(undefined);
49
51
  }
50
52
  /**
51
53
  * A border row: the opening corner, the rule across `span` cells with `label`
@@ -129,7 +131,7 @@ export class Panel {
129
131
  // and the content is rendered on top of it. Applied to the padding alone,
130
132
  // "white on dark_blue" drew a blue ring with a hole where the text was.
131
133
  const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
132
- const border = borderStyle.isNull ? undefined : borderStyle;
134
+ const border = box.frame(borderStyle);
133
135
  const geometry = layoutPanel(this._getPanelWidth(options));
134
136
  const bodyLines = this._renderBody(options, geometry.spanWidth);
135
137
  yield* this._renderTopBorder(options, box, geometry, border);
@@ -241,7 +243,7 @@ export class Panel {
241
243
  const corner = box.bottom.right.repeat(geometry.right);
242
244
  const [close, afterAccessory] = accessoryText === "" ? [corner, ""] : ["", corner];
243
245
  yield* borderRow(options, box.bottom, geometry, geometry.spanWidth - cellLen(accessoryText), this._subtitleLabel, subtitleStyle, border, close);
244
- yield new Segment(accessoryText, borderTextStyle(border, accessoryOwn));
246
+ yield* labelled("", accessoryText, "", borderTextStyle(border, accessoryOwn), options.textOrder);
245
247
  yield new Segment(afterAccessory, border);
246
248
  yield Segment.line();
247
249
  }
@@ -44,9 +44,9 @@ export declare class ProgressBar implements Renderable, Measurable {
44
44
  set finished(value: boolean | undefined);
45
45
  render(options: RenderOptions): Iterable<Segment>;
46
46
  /**
47
- * The shimmer over this bar's back colour as `Effected` sees it drawn at
48
- * `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
49
- * back takes the whole of the light.
47
+ * The shimmer over this bar's back colour as `Effected` sees it drawn,
48
+ * toward `bar.pulse`. A bar has no words to keep legible, so its back takes
49
+ * the whole of the light.
50
50
  */
51
51
  private pulseEffect;
52
52
  /** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
@@ -71,30 +71,33 @@ export class ProgressBar {
71
71
  }
72
72
  const depth = options.colorSystem ?? ColorDepth.TRUECOLOR;
73
73
  // Rich's pulse resolves its colours on the terminal a depth assumes, as
74
- // `get_truecolor` does with no theme; so does this.
75
- const theme = resolveTerminal(undefined, depth);
74
+ // `get_truecolor` does with no theme; so does this with none named.
75
+ const terminal = resolveTerminal(options.terminal, depth);
76
76
  // Rich writes the pulse's back as a colour of its own, black where the
77
77
  // back style names none, so the light always has the bar's colour to fall
78
78
  // on: one the terminal supplies takes no light.
79
79
  const style = getStyle(options, this.style);
80
- const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(theme, true));
80
+ const ink = style.color?.isDefault === false ? style.color : ColorSpec.fromRgba((style.color ?? PULSE_BACK).getTruecolor(terminal, true));
81
81
  const back = style.add(Style.fromColor(ink));
82
82
  // [LAW:no-ambient-temporal-coupling] The moment is the frame's; the bar
83
83
  // only says what it looks like then. An empty track, lit by the shimmer.
84
84
  const track = { render: (o) => this.track(o, width, 0, back) };
85
- yield* render(new Effected(track, this.pulseEffect(options, back, depth, theme), { key: "progress-bar", theme }), options);
85
+ yield* render(new Effected(track, this.pulseEffect(options, back), { key: "progress-bar" }), options);
86
86
  }
87
87
  /**
88
- * The shimmer over this bar's back colour as `Effected` sees it drawn at
89
- * `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
90
- * back takes the whole of the light.
88
+ * The shimmer over this bar's back colour as `Effected` sees it drawn,
89
+ * toward `bar.pulse`. A bar has no words to keep legible, so its back takes
90
+ * the whole of the light.
91
91
  */
92
- pulseEffect(options, back, depth, theme) {
93
- const ink = back.drawnColors(depth).color ?? ColorSpec.default();
94
- const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
95
- const loop = shimmer(PULSE_CURVE, PULSE_REACH, glow.getTruecolor(theme, true), 0);
96
- const whole = new Map([[ink.getTruecolor(theme, true).hex, 1]]);
97
- return atShares(loop, () => whole);
92
+ pulseEffect(options, back) {
93
+ return (drawn) => {
94
+ const { depth, terminal } = drawn;
95
+ const ink = back.drawnColors(depth).color ?? ColorSpec.default();
96
+ const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
97
+ const loop = shimmer(PULSE_CURVE, PULSE_REACH, glow.getTruecolor(terminal, true), 0);
98
+ const whole = new Map([[ink.getTruecolor(terminal, true).hex, 1]]);
99
+ return atShares(loop, () => whole)(drawn);
100
+ };
98
101
  }
99
102
  /** Rich 9d8f9a3 `__rich_console__`: the fill is counted in half cells, and a total of zero is a bar already full. */
100
103
  completeHalves(width) {
@@ -7,7 +7,8 @@
7
7
  * other renderable here is restyled, and a second styling vocabulary on this
8
8
  * one class would be a second answer to "what colour is a keyword".
9
9
  */
10
- import { cellLen, expandTabs, parseTabSize } from "../core/cells.js";
10
+ import { escaped } from "../core/bidi.js";
11
+ import { cellLen, expandTabs, parseTabSize, unicodeEscape } from "../core/cells.js";
11
12
  import { Segment } from "../core/segment.js";
12
13
  import { RichText } from "../core/text.js";
13
14
  import { drawable, groundedStyle, getStyle, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
@@ -89,7 +90,11 @@ export class Syntax {
89
90
  this.wordWrap = options?.wordWrap ?? false;
90
91
  const tabSize = parseTabSize(options?.tabSize ?? 4);
91
92
  this.tabSize = tabSize;
92
- this._lines = expandTabs(code, tabSize).text.split("\n");
93
+ // Code on show: what the wire would drop is escaped, and an explicit bidi
94
+ // control with it. That is the Trojan Source attack: the order and the lines on
95
+ // screen differing from what the compiler reads. A joiner stays, since a Persian
96
+ // comment or an emoji string needs it. Only a CRLF's CR goes, with its line ending.
97
+ this._lines = expandTabs(escaped(code.replace(/\r\n/g, "\n"), unicodeEscape), tabSize).text.split("\n");
93
98
  }
94
99
  /**
95
100
  * The lines shown, as a 0-based half-open slice of `_lines`, and the number
@@ -454,11 +454,12 @@ export class Table {
454
454
  // that is all it styles: the reference reads it as `table_style` for the
455
455
  // frame and nowhere else, so a cell is untouched by it.
456
456
  const borderStyle = getStyle(options, this.style).add(getStyle(options, this.borderStyle));
457
- const border = borderStyle.isNull ? undefined : borderStyle;
458
457
  // The one division of the width every row below is measured against.
459
458
  const outerWidth = this._outerWidth(options);
460
459
  const geometry = this._geometry(options, outerWidth);
461
460
  const edge = geometry.edge === 1;
461
+ // Only an edge closes a frame: without one, a table's rules are not a border.
462
+ const border = box && edge ? box.frame(borderStyle) : borderStyle.isNull ? undefined : borderStyle;
462
463
  // Title
463
464
  if (this._title) {
464
465
  yield* this._renderTitle(options, this._title, geometry.totalWidth, this.titleStyle, this.titleJustify);