@promptctl/rich-js 0.23.0 → 0.24.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 (51) hide show
  1. package/dist/core/clock.d.ts +9 -1
  2. package/dist/core/clock.js +22 -2
  3. package/dist/core/color.d.ts +10 -0
  4. package/dist/core/color.js +36 -1
  5. package/dist/core/easing.d.ts +31 -0
  6. package/dist/core/easing.js +106 -1
  7. package/dist/core/segment.d.ts +10 -0
  8. package/dist/core/segment.js +15 -0
  9. package/dist/core/strip.d.ts +104 -27
  10. package/dist/core/strip.js +231 -124
  11. package/dist/core/style.d.ts +18 -1
  12. package/dist/core/style.js +32 -3
  13. package/dist/core/text.js +4 -1
  14. package/dist/host/app.js +2 -4
  15. package/dist/host/index.d.ts +1 -1
  16. package/dist/host/input.d.ts +37 -13
  17. package/dist/host/input.js +146 -56
  18. package/dist/host/terminal-host.d.ts +16 -0
  19. package/dist/host/terminal-host.js +1 -0
  20. package/dist/index.d.ts +11 -9
  21. package/dist/index.js +5 -4
  22. package/dist/node/terminal-host.d.ts +8 -1
  23. package/dist/node/terminal-host.js +2 -0
  24. package/dist/renderables/effect.d.ts +78 -10
  25. package/dist/renderables/effect.js +143 -39
  26. package/dist/renderables/effects.d.ts +73 -26
  27. package/dist/renderables/effects.js +412 -92
  28. package/dist/renderables/embed.d.ts +10 -0
  29. package/dist/renderables/embed.js +15 -7
  30. package/dist/renderables/frames.d.ts +95 -0
  31. package/dist/renderables/frames.js +106 -0
  32. package/dist/renderables/live.d.ts +23 -7
  33. package/dist/renderables/live.js +68 -34
  34. package/dist/renderables/markdown.d.ts +5 -2
  35. package/dist/renderables/markdown.js +210 -19
  36. package/dist/renderables/progress.d.ts +1 -1
  37. package/dist/renderables/progress.js +3 -1
  38. package/dist/renderables/progressBar.js +3 -3
  39. package/dist/renderables/spinner.d.ts +62 -28
  40. package/dist/renderables/spinner.js +99 -52
  41. package/dist/renderables/status.d.ts +8 -3
  42. package/dist/renderables/status.js +8 -5
  43. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  44. package/dist/template-bindings/animation-funcs.js +83 -0
  45. package/dist/template-bindings/color-funcs.d.ts +13 -0
  46. package/dist/template-bindings/color-funcs.js +1 -1
  47. package/dist/template-bindings/helpers.d.ts +6 -0
  48. package/dist/template-bindings/helpers.js +12 -3
  49. package/dist/template-bindings/index.d.ts +6 -2
  50. package/dist/template-bindings/index.js +6 -2
  51. package/package.json +1 -1
@@ -9,17 +9,28 @@
9
9
  *
10
10
  * [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
11
11
  * input chunk: append → consume one event at a time until the buffer can't
12
- * yield more. The byte's value, not a side-mode, decides which event is
13
- * emitted. There is no "are we mid-sequence" mode flag — the buffer head
14
- * carries all the state we need, so a UTF-8 character or an escape sequence
15
- * split across two chunks is completed by the second.
12
+ * yield more. The byte's value decides which event is emitted; there is no
13
+ * "are we mid-sequence" mode flag — the buffer head carries that state, so on
14
+ * a host whose chunks can split a key, a UTF-8 character or an escape
15
+ * sequence split across two chunks is completed by the second. What a chunk
16
+ * end means is the host's `InputChunks`, the one input to the parser that is
17
+ * not a byte, and it decides only what becomes of an unfinished ESC.
16
18
  *
17
- * Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
18
- * CSI sequence). When the buffer is drained but ends with a lone ESC, the
19
- * parser defers emission via setTimeout(0). A next chunk opening with `[` or
20
- * `O` extends the sequence; any other next chunk emits the ESC as Escape
21
- * first. `flush()` drains it synchronously.
19
+ * An unfinished escape sequence is ambiguous: `ESC` alone is the Escape key
20
+ * or the start of a sequence, and `ESC [` is Alt+[ or the start of a CSI.
21
+ * Only the host's `InputChunks` and the clock can settle it. On a
22
+ * `"whole-keys"` host a chunk ends on a key boundary, so an ESC still waiting
23
+ * at a chunk's end opened nothing: it is Escape, and what followed it is read
24
+ * again on its own. On a `"split"` host a read can end anywhere, so the
25
+ * parser holds the unfinished sequence for the escape delay — ncurses'
26
+ * ESCDELAY, tmux's escape-time — and, when nothing has extended it by then,
27
+ * settles it by the same whole-keys rule. `ESC ESC` is held too, since the
28
+ * byte after it decides whether it is Alt on a sequence or one Escape. A held
29
+ * lone ESC is also settled early by a next chunk that opens with anything but
30
+ * `[` or `O`, because a busy worker can queue the next key behind it.
31
+ * `flush()` settles the hold synchronously.
22
32
  */
33
+ import type { InputChunks } from "./terminal-host.js";
23
34
  /** A key as the terminal reported it: plain data, with no say over who hears it. */
24
35
  export interface KeyPress {
25
36
  readonly key: string;
@@ -63,18 +74,31 @@ export interface InputSink {
63
74
  * one place a program hands names in, rather than binding a key that never fires.
64
75
  */
65
76
  export declare function parseKeyName(name: string): string;
77
+ /**
78
+ * How long a `"split"` host's parser waits for the rest of an unfinished
79
+ * escape sequence before reading it as the keys it already holds. Textual's
80
+ * ESCAPE_DELAY and vim's `ttimeoutlen` default to the same: long enough for
81
+ * the rest of a sequence a read split (over ssh, say) to arrive, short
82
+ * enough that Escape does not feel late.
83
+ */
84
+ export declare const ESCAPE_DELAY_MS = 100;
66
85
  export declare class InputParser {
67
86
  private readonly sink;
87
+ private readonly chunks;
68
88
  private buffer;
69
89
  private escTimer;
70
- constructor(sink: InputSink);
90
+ constructor(sink: InputSink, chunks: InputChunks);
71
91
  /** Feed a chunk of bytes (or a string of them) into the parser. */
72
92
  feed(chunk: Uint8Array | string): void;
73
- /** Force any pending lone ESC out of the buffer. */
93
+ private drainAndHold;
94
+ private hold;
95
+ private settleChunk;
96
+ private drainFully;
97
+ /** Settle a held escape sequence now, as the end of a whole-key chunk. */
74
98
  flush(): void;
75
- /** Drop what is buffered and any pending lone ESC: the parser as constructed. */
99
+ /** Drop what is buffered and any held escape sequence: the parser as constructed. */
76
100
  reset(): void;
77
- /** The parser's one hold: a lone ESC that may yet open a sequence. */
101
+ /** A lone ESC, the one hold a next chunk can settle before its bytes are read. */
78
102
  private holdingEscape;
79
103
  private cancelEscape;
80
104
  private drain;
@@ -9,16 +9,26 @@
9
9
  *
10
10
  * [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
11
11
  * input chunk: append → consume one event at a time until the buffer can't
12
- * yield more. The byte's value, not a side-mode, decides which event is
13
- * emitted. There is no "are we mid-sequence" mode flag — the buffer head
14
- * carries all the state we need, so a UTF-8 character or an escape sequence
15
- * split across two chunks is completed by the second.
12
+ * yield more. The byte's value decides which event is emitted; there is no
13
+ * "are we mid-sequence" mode flag — the buffer head carries that state, so on
14
+ * a host whose chunks can split a key, a UTF-8 character or an escape
15
+ * sequence split across two chunks is completed by the second. What a chunk
16
+ * end means is the host's `InputChunks`, the one input to the parser that is
17
+ * not a byte, and it decides only what becomes of an unfinished ESC.
16
18
  *
17
- * Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
18
- * CSI sequence). When the buffer is drained but ends with a lone ESC, the
19
- * parser defers emission via setTimeout(0). A next chunk opening with `[` or
20
- * `O` extends the sequence; any other next chunk emits the ESC as Escape
21
- * first. `flush()` drains it synchronously.
19
+ * An unfinished escape sequence is ambiguous: `ESC` alone is the Escape key
20
+ * or the start of a sequence, and `ESC [` is Alt+[ or the start of a CSI.
21
+ * Only the host's `InputChunks` and the clock can settle it. On a
22
+ * `"whole-keys"` host a chunk ends on a key boundary, so an ESC still waiting
23
+ * at a chunk's end opened nothing: it is Escape, and what followed it is read
24
+ * again on its own. On a `"split"` host a read can end anywhere, so the
25
+ * parser holds the unfinished sequence for the escape delay — ncurses'
26
+ * ESCDELAY, tmux's escape-time — and, when nothing has extended it by then,
27
+ * settles it by the same whole-keys rule. `ESC ESC` is held too, since the
28
+ * byte after it decides whether it is Alt on a sequence or one Escape. A held
29
+ * lone ESC is also settled early by a next chunk that opens with anything but
30
+ * `[` or `O`, because a busy worker can queue the next key behind it.
31
+ * `flush()` settles the hold synchronously.
22
32
  */
23
33
  /**
24
34
  * The one name a key goes by — what a keymap is keyed by and what
@@ -137,6 +147,14 @@ const UTF8_ENCODER = new TextEncoder();
137
147
  const UTF8_DECODER = new TextDecoder("utf-8");
138
148
  const ASCII_DECODER = new TextDecoder("ascii");
139
149
  const EMPTY_BYTES = new Uint8Array(0);
150
+ /**
151
+ * How long a `"split"` host's parser waits for the rest of an unfinished
152
+ * escape sequence before reading it as the keys it already holds. Textual's
153
+ * ESCAPE_DELAY and vim's `ttimeoutlen` default to the same: long enough for
154
+ * the rest of a sequence a read split (over ssh, say) to arrive, short
155
+ * enough that Escape does not feel late.
156
+ */
157
+ export const ESCAPE_DELAY_MS = 100;
140
158
  // Concatenate two byte arrays into a fresh Uint8Array. Replaces
141
159
  // `Buffer.concat([...])` so the parser holds no node-specific globals.
142
160
  function concatBytes(a, b) {
@@ -149,8 +167,30 @@ function concatBytes(a, b) {
149
167
  merged.set(b, a.length);
150
168
  return merged;
151
169
  }
170
+ // Alt on a printable byte: the glyph the terminal sent, with meta held.
171
+ function altKey(byte) {
172
+ return { key: String.fromCharCode(byte), character: "", shift: false, ctrl: false, meta: true };
173
+ }
174
+ // ECMA-48: an escape sequence is built of bytes 0x20–0x7E after its ESC, so
175
+ // any other byte (ESC itself, a control, a UTF-8 byte) ends one unfinished.
176
+ function inSequence(byte) {
177
+ return byte >= 0x20 && byte <= 0x7e;
178
+ }
179
+ // [LAW:one-source-of-truth] An escape sequence that opened no key — ended
180
+ // before its final byte (at a whole-key chunk's end, at the escape delay, or at
181
+ // a byte that cannot belong to it), or an SS3 no terminal sends — is reissued
182
+ // as the keys it holds, as Textual reissues one. `ESC ESC` is one Escape, as a
183
+ // terminal that double-sends it means, whatever followed it; `ESC [` / `ESC O`
184
+ // alone is the whole key Alt+[ / Alt+O; any other ESC is Escape. What followed
185
+ // the Escape is read again on its own.
186
+ function reissue(sequence) {
187
+ if (sequence[1] === ESC)
188
+ return { bytes: 2, event: ESCAPE };
189
+ return sequence.length === 2 ? { bytes: 2, event: altKey(sequence[1]) } : { bytes: 1, event: ESCAPE };
190
+ }
152
191
  export class InputParser {
153
192
  sink;
193
+ chunks;
154
194
  // [LAW:types-are-the-program] The parse buffer is `Uint8Array`, not
155
195
  // `Buffer` — node's Buffer extends Uint8Array, so Buffer chunks from
156
196
  // process.stdin satisfy this type, while a browser host can emit native
@@ -159,12 +199,17 @@ export class InputParser {
159
199
  // [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
160
200
  // across node and browser; `setImmediate` is node-only.
161
201
  escTimer;
162
- constructor(sink) {
202
+ constructor(sink, chunks) {
163
203
  this.sink = sink;
204
+ this.chunks = chunks;
164
205
  }
165
206
  /** Feed a chunk of bytes (or a string of them) into the parser. */
166
207
  feed(chunk) {
167
208
  const next = typeof chunk === "string" ? UTF8_ENCODER.encode(chunk) : chunk;
209
+ if (this.chunks === "whole-keys") {
210
+ this.buffer = concatBytes(this.buffer, next);
211
+ return this.settleChunk();
212
+ }
168
213
  // A held lone ESC waits only for the rest of a split CSI/SS3 introducer.
169
214
  // Any other next chunk means Escape was its own key, even when the hold
170
215
  // timer has not run yet: a busy worker can queue the next key ahead of it,
@@ -179,32 +224,77 @@ export class InputParser {
179
224
  this.sink.key(ESCAPE);
180
225
  }
181
226
  finally {
227
+ this.drainAndHold();
228
+ }
229
+ }
230
+ drainAndHold() {
231
+ try {
232
+ this.drainFully();
233
+ }
234
+ finally {
235
+ this.hold();
236
+ }
237
+ }
238
+ // [LAW:no-ambient-temporal-coupling] Whatever drain leaves at an ESC is an
239
+ // unfinished sequence, and the escape delay is what tells one whose rest is
240
+ // still in flight from one that stalled: a stalled `ESC [` held for good
241
+ // would read the next key as its final byte (rich-host-input-gnl7). The delay
242
+ // counts time the program could have read the rest in: node runs an expired
243
+ // timer before it reads input queued behind a long frame, so the delay's end
244
+ // settles the hold only after one more turn of the event loop has read what
245
+ // was waiting. The parser holds one timer, so arming it replaces any other.
246
+ hold() {
247
+ this.cancelEscape();
248
+ if (this.buffer[0] !== ESC)
249
+ return;
250
+ this.escTimer = setTimeout(() => {
251
+ this.escTimer = setTimeout(() => this.flush(), 0);
252
+ }, ESCAPE_DELAY_MS);
253
+ }
254
+ // A whole-key chunk has ended, so an ESC still at the buffer head opens no
255
+ // sequence, and is read as `reissue` reads it. Each key is taken off the
256
+ // buffer before the sink hears it, so no byte of one chunk outlives it.
257
+ settleChunk() {
258
+ try {
259
+ this.drainFully();
260
+ }
261
+ finally {
262
+ if (this.buffer[0] === ESC) {
263
+ const { bytes, event } = reissue(this.buffer);
264
+ this.buffer = this.buffer.subarray(bytes);
265
+ try {
266
+ this.sink.key(event);
267
+ }
268
+ finally {
269
+ this.settleChunk();
270
+ }
271
+ }
272
+ }
273
+ }
274
+ // Drain, and when a handler throws, read the rest of the buffer in full
275
+ // before the throw goes on, so a throwing handler strands no key behind it.
276
+ drainFully() {
277
+ let drained = false;
278
+ try {
182
279
  this.drain();
280
+ drained = true;
183
281
  }
184
- // [LAW:dataflow-not-control-flow] If a lone ESC remains, defer emission
185
- // until the next macrotask so a follow-up chunk can extend it. Only a lone
186
- // one: a sequence split after `ESC [` waits for its rest, however late.
187
- if (this.holdingEscape()) {
188
- this.escTimer = setTimeout(() => {
189
- this.escTimer = undefined;
190
- this.flush();
191
- }, 0);
282
+ finally {
283
+ if (!drained)
284
+ this.drainFully();
192
285
  }
193
286
  }
194
- /** Force any pending lone ESC out of the buffer. */
287
+ /** Settle a held escape sequence now, as the end of a whole-key chunk. */
195
288
  flush() {
196
289
  this.cancelEscape();
197
- if (this.holdingEscape()) {
198
- this.buffer = EMPTY_BYTES;
199
- this.sink.key(ESCAPE);
200
- }
290
+ this.settleChunk();
201
291
  }
202
- /** Drop what is buffered and any pending lone ESC: the parser as constructed. */
292
+ /** Drop what is buffered and any held escape sequence: the parser as constructed. */
203
293
  reset() {
204
294
  this.cancelEscape();
205
295
  this.buffer = EMPTY_BYTES;
206
296
  }
207
- /** The parser's one hold: a lone ESC that may yet open a sequence. */
297
+ /** A lone ESC, the one hold a next chunk can settle before its bytes are read. */
208
298
  holdingEscape() {
209
299
  return this.buffer.length === 1 && this.buffer[0] === ESC;
210
300
  }
@@ -218,7 +308,7 @@ export class InputParser {
218
308
  if (result.kind === "incomplete")
219
309
  return;
220
310
  this.buffer = this.buffer.subarray(result.bytes);
221
- if (result.kind === "key")
311
+ if (result.kind === "key" || result.kind === "reissued")
222
312
  this.sink.key(result.event);
223
313
  else if (result.kind === "mouse")
224
314
  this.sink.mouse(result.event);
@@ -291,30 +381,28 @@ export class InputParser {
291
381
  // ESC O X → SS3 (F1–F4 / arrows on some terms)
292
382
  if (b1 === O_BYTE)
293
383
  return this.consumeSS3(buf);
294
- // ESC ESC [ … / ESC ESC O … → Alt on a key the terminal sends as a
295
- // sequence (macOS Terminal and iTerm send Option+Up as `ESC ESC [ A`): the
296
- // inner sequence, with meta held.
297
- if (b1 === ESC && buf.length > 2 && (buf[2] === LBRACKET || buf[2] === O_BYTE)) {
298
- // A pointer report is never Alt-prefixed (SGR carries modifiers in its
299
- // button byte), so before anything but a key the first ESC is Escape.
300
- const inner = this.consumeEscape(buf.subarray(1));
301
- if (inner.kind === "incomplete")
302
- return inner;
303
- if (inner.kind === "key") {
304
- return { kind: "key", bytes: inner.bytes + 1, event: { ...inner.event, meta: true } };
305
- }
306
- return { kind: "key", bytes: 1, event: ESCAPE };
307
- }
308
- // ESC ESC → escape key (some terminals double-send). Consume BOTH bytes
309
- // so the user sees one escape event, not two. Without this, the second
310
- // ESC remained in the buffer and produced a second escape via the
311
- // lone-ESC flush path.
312
384
  if (b1 === ESC) {
313
- return {
314
- kind: "key",
315
- bytes: 2,
316
- event: ESCAPE,
317
- };
385
+ // The byte after `ESC ESC` decides what it opens, so a read that ends
386
+ // between them leaves it unfinished, held like any other.
387
+ if (buf.length === 2)
388
+ return { kind: "incomplete" };
389
+ // ESC ESC [ … / ESC ESC O … → Alt on a key the terminal sends as a
390
+ // sequence (macOS Terminal and iTerm send Option+Up as `ESC ESC [ A`):
391
+ // the inner sequence, with meta held.
392
+ if (buf[2] === LBRACKET || buf[2] === O_BYTE) {
393
+ const inner = this.consumeEscape(buf.subarray(1));
394
+ if (inner.kind === "incomplete")
395
+ return inner;
396
+ if (inner.kind === "key") {
397
+ return { kind: "key", bytes: inner.bytes + 1, event: { ...inner.event, meta: true } };
398
+ }
399
+ // A pointer report is never Alt-prefixed (SGR carries modifiers in its
400
+ // button byte): the second ESC is the report's, and the first is Escape.
401
+ if (inner.kind !== "reissued")
402
+ return { kind: "key", bytes: 1, event: ESCAPE };
403
+ }
404
+ // `ESC ESC` before a byte that opens no key opened none itself.
405
+ return { kind: "reissued", ...reissue(buf) };
318
406
  }
319
407
  // ESC <named> / ESC <printable> → Alt-modified key.
320
408
  // Terminals encode Alt+<key> as the literal byte preceded by ESC, so
@@ -352,12 +440,7 @@ export class InputParser {
352
440
  // The key is the glyph the terminal sent, as for Alt+`>`: the glyph
353
441
  // already carries shift, so Alt+G is `meta+G` and never `meta+shift+g`.
354
442
  if (b1 >= 0x20 && b1 <= 0x7e) {
355
- const ch = String.fromCharCode(b1);
356
- return {
357
- kind: "key",
358
- bytes: 2,
359
- event: ({ key: ch, character: "", shift: false, ctrl: false, meta: true }),
360
- };
443
+ return { kind: "key", bytes: 2, event: altKey(b1) };
361
444
  }
362
445
  // ESC <control byte> is read as Escape, then the Ctrl chord — never as
363
446
  // Alt+Ctrl. The bytes cannot tell the two apart, and Escape then Ctrl+C
@@ -372,10 +455,13 @@ export class InputParser {
372
455
  consumeSS3(buf) {
373
456
  if (buf.length < 3)
374
457
  return { kind: "incomplete" };
458
+ if (!inSequence(buf[2]))
459
+ return { kind: "reissued", ...reissue(buf.subarray(0, 2)) };
375
460
  const final = String.fromCharCode(buf[2]);
376
461
  const name = SS3_KEYS[final];
462
+ // No terminal reports in SS3, so an unknown final was typed: Escape, then O.
377
463
  if (!name)
378
- return { kind: "skip", bytes: 3 };
464
+ return { kind: "reissued", ...reissue(buf.subarray(0, 3)) };
379
465
  return {
380
466
  kind: "key",
381
467
  bytes: 3,
@@ -418,6 +504,8 @@ export class InputParser {
418
504
  event: decodeMouseFromCb(cb, col, row, b === M_UPPER),
419
505
  };
420
506
  }
507
+ if (!inSequence(b))
508
+ return { kind: "reissued", ...reissue(buf.subarray(0, i)) };
421
509
  }
422
510
  return { kind: "incomplete" };
423
511
  }
@@ -430,6 +518,8 @@ export class InputParser {
430
518
  const final = String.fromCharCode(b);
431
519
  return decodeCSI(params, final, i + 1);
432
520
  }
521
+ if (!inSequence(b))
522
+ return { kind: "reissued", ...reissue(buf.subarray(0, i)) };
433
523
  params += String.fromCharCode(b);
434
524
  }
435
525
  return { kind: "incomplete" };
@@ -43,6 +43,13 @@ export interface TerminalSize {
43
43
  readonly rows: number;
44
44
  }
45
45
  export type DataHandler = (chunk: Uint8Array | string) => void;
46
+ /**
47
+ * Where a host's input chunks may end. `"whole-keys"`: every chunk ends on a
48
+ * key boundary, so no escape sequence is ever split across two (xterm.js
49
+ * emits one key, or one paste, per event). `"split"`: a chunk may end
50
+ * anywhere, as a read from a tty can.
51
+ */
52
+ export type InputChunks = "whole-keys" | "split";
46
53
  export type ResizeHandler = (size: TerminalSize) => void;
47
54
  export type ExitHandler = () => void;
48
55
  export interface TerminalHost {
@@ -60,6 +67,14 @@ export interface TerminalHost {
60
67
  * runtime's parser accepts either.
61
68
  */
62
69
  onData(handler: DataHandler): Unsubscribe;
70
+ /**
71
+ * Where `onData`'s chunks may end. The parser holds an unfinished escape
72
+ * sequence open for its rest, for the escape delay, only on a host whose
73
+ * chunks can split one; a whole-keys host settles each chunk as it arrives,
74
+ * because a busy worker there reads a queued next key before any timer
75
+ * runs (rich-host-input-ab9n).
76
+ */
77
+ readonly inputChunks: InputChunks;
63
78
  /**
64
79
  * Subscribe to terminal resize events. The handler fires with the new
65
80
  * size after a resize is observed.
@@ -176,6 +191,7 @@ export declare class BrowserTerminalHost implements TerminalHost {
176
191
  private dataSubscription;
177
192
  private resizeSubscription;
178
193
  readonly passWheel: (lines: number) => void;
194
+ readonly inputChunks = "whole-keys";
179
195
  constructor(options: BrowserTerminalHostOptions);
180
196
  get isTTY(): boolean;
181
197
  get writesToTerminal(): boolean;
@@ -47,6 +47,7 @@ export class BrowserTerminalHost {
47
47
  dataSubscription;
48
48
  resizeSubscription;
49
49
  passWheel;
50
+ inputChunks = "whole-keys";
50
51
  constructor(options) {
51
52
  this.terminal = options.terminal;
52
53
  this.passWheel = options.passWheel;
package/dist/index.d.ts CHANGED
@@ -15,8 +15,8 @@ export { transposePalette, themeKeyForRoot, isAnchored, ANCHORED_ROOTS, } from "
15
15
  export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureContrast, ensureDrawn, drawnColour, } from "./themes/colorMath.js";
16
16
  export { ColorRamp } from "./themes/ramp.js";
17
17
  export type { ColorStop } from "./themes/ramp.js";
18
- export { EASES, Phase, cubicBezier, parseEase, steps } from "./core/easing.js";
19
- export type { Ease, StepPosition } from "./core/easing.js";
18
+ export { EASES, Phase, beat, cubicBezier, parseEase, steps } from "./core/easing.js";
19
+ export type { Beat, Ease, StepPosition } from "./core/easing.js";
20
20
  export { frameRate, systemClock } from "./core/clock.js";
21
21
  export type { Clock, FrameRate } from "./core/clock.js";
22
22
  export { Style, StyleSyntaxError, StyleStack, Theme, NULL_STYLE, DEFAULT_STYLES, } from "./core/style.js";
@@ -33,8 +33,8 @@ export type { Unsubscribe } from "./core/subscription.js";
33
33
  export { Measurement, measureRenderables } from "./core/measure.js";
34
34
  export { Span, RichText } from "./core/text.js";
35
35
  export type { RichTextOptions } from "./core/text.js";
36
- export { Strip, CellJoiner, SEAM_MIN_DELTA_E, JOINERS, joinerNamed, gradientJoiner, } from "./core/strip.js";
37
- export type { StyledRenderable, Joiner, JoinContext, JoinShape, JoinCell, CellColor, Paint } from "./core/strip.js";
36
+ export { Strip, CellJoiner, SEAM_MIN_DELTA_E, SEAMS, CAPS, JOINERS, joinerNamed, gradientSeam, } from "./core/strip.js";
37
+ export type { StyledRenderable, Joiner, JoinContext, JoinSeam, JoinCaps, SeamForm, CapsForm, SeamCell, JoinCell, CellColor, Paint, JoinerSpec, } from "./core/strip.js";
38
38
  export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
39
39
  export type { RenderToStringOptions } from "./core/render.js";
40
40
  export { OSC8, osc8Sequences } from "./core/osc8.js";
@@ -55,10 +55,10 @@ export { Console } from "./core/console.js";
55
55
  export type { ConsoleOptions, ConsoleSink, ConsoleStream, ConsoleEnvironment, LiveRegion, PrintOptions, } from "./core/console.js";
56
56
  export type { Env } from "./core/env.js";
57
57
  export { Constrain } from "./renderables/constrain.js";
58
- export { Effected, confined, drawnOf } from "./renderables/effect.js";
59
- export type { CellColors, Drawn, Effect, EffectCell, EffectedOptions, Pair } from "./renderables/effect.js";
60
- export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, wheel, } from "./renderables/effects.js";
61
- export type { Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
58
+ export { Effected, confined, drawnOf, nextChangeOf, onBeat, withNextChange } from "./renderables/effect.js";
59
+ export type { CellColors, Drawn, Effect, EffectCell, EffectedOptions, Pair, Placed } from "./renderables/effect.js";
60
+ export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
61
+ export type { ColorStep, Curve, EffectTarget, Field, Loop, LoopName, Touch, Transition, TransitionName } from "./renderables/effects.js";
62
62
  export { Align } from "./renderables/align.js";
63
63
  export type { Alignment } from "./core/place.js";
64
64
  export { Padding } from "./renderables/padding.js";
@@ -77,6 +77,8 @@ export { ProgressBar } from "./renderables/progressBar.js";
77
77
  export type { ProgressBarOptions } from "./renderables/progressBar.js";
78
78
  export { Spinner } from "./renderables/spinner.js";
79
79
  export type { SpinnerOptions } from "./renderables/spinner.js";
80
+ export { FrameSequence, Frames } from "./renderables/frames.js";
81
+ export type { Frame, FramesOptions } from "./renderables/frames.js";
80
82
  export { Table, Column } from "./renderables/table.js";
81
83
  export type { TableOptions, ColumnOptions } from "./renderables/table.js";
82
84
  export { Tree } from "./renderables/tree.js";
@@ -86,7 +88,7 @@ export type { ColumnsOptions } from "./renderables/columns.js";
86
88
  export { FlexStrip } from "./renderables/flexStrip.js";
87
89
  export type { FlexStripOptions, FlexAlign } from "./renderables/flexStrip.js";
88
90
  export { Live } from "./renderables/live.js";
89
- export type { LiveOptions } from "./renderables/live.js";
91
+ export type { LiveOptions, LiveView } from "./renderables/live.js";
90
92
  export { Status } from "./renderables/status.js";
91
93
  export type { StatusOptions } from "./renderables/status.js";
92
94
  export { Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn, SpinnerColumn, MofNCompleteColumn, track, } from "./renderables/progress.js";
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ export { lighten, darken, relativeLuminance, contrastRatio, contrastFor, ensureC
29
29
  export { ColorRamp } from "./themes/ramp.js";
30
30
  // Progress in [0, 1]: phases turn seconds into it, eases reshape it. One
31
31
  // vocabulary for ramps and time-based effects alike, CSS's names throughout.
32
- export { EASES, Phase, cubicBezier, parseEase, steps } from "./core/easing.js";
32
+ export { EASES, Phase, beat, cubicBezier, parseEase, steps } from "./core/easing.js";
33
33
  // The time source and timer whoever draws frames is handed, and the one
34
34
  // parser of the rate it ticks at.
35
35
  export { frameRate, systemClock } from "./core/clock.js";
@@ -44,7 +44,7 @@ export { Measurement, measureRenderables } from "./core/measure.js";
44
44
  // Text
45
45
  export { Span, RichText } from "./core/text.js";
46
46
  // Strip + Joiner
47
- export { Strip, CellJoiner, SEAM_MIN_DELTA_E, JOINERS, joinerNamed, gradientJoiner, } from "./core/strip.js";
47
+ export { Strip, CellJoiner, SEAM_MIN_DELTA_E, SEAMS, CAPS, JOINERS, joinerNamed, gradientSeam, } from "./core/strip.js";
48
48
  // renderToString — stateless one-shot emission
49
49
  export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
50
50
  // OSC 8 hyperlink wire grammar — for consumers that read rendered bytes back
@@ -69,8 +69,8 @@ export { SPINNERS, DEFAULT_SPINNER } from "./core/spinnerData.js";
69
69
  export { Console } from "./core/console.js";
70
70
  // Renderables
71
71
  export { Constrain } from "./renderables/constrain.js";
72
- export { Effected, confined, drawnOf } from "./renderables/effect.js";
73
- export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, wheel, } from "./renderables/effects.js";
72
+ export { Effected, confined, drawnOf, nextChangeOf, onBeat, withNextChange } from "./renderables/effect.js";
73
+ export { EFFECT_CATALOGUE, EFFECT_CURVES, EFFECT_LIGHTS, dissolveOut, fadeIn, onColors, pulse, shimmer, sparkle, steppedInk, wheel, } from "./renderables/effects.js";
74
74
  export { Align } from "./renderables/align.js";
75
75
  export { Padding } from "./renderables/padding.js";
76
76
  export { Viewport, ViewportPerKey, SCROLLBAR } from "./renderables/viewport.js";
@@ -81,6 +81,7 @@ export { Panel } from "./renderables/panel.js";
81
81
  export { Group } from "./renderables/group.js";
82
82
  export { ProgressBar } from "./renderables/progressBar.js";
83
83
  export { Spinner } from "./renderables/spinner.js";
84
+ export { FrameSequence, Frames } from "./renderables/frames.js";
84
85
  export { Table, Column } from "./renderables/table.js";
85
86
  export { Tree } from "./renderables/tree.js";
86
87
  export { Columns } from "./renderables/columns.js";
@@ -27,7 +27,7 @@
27
27
  * `setRawMode` in the runtime or in a demo, take a `TerminalHost` parameter
28
28
  * instead. The seam is already here.
29
29
  */
30
- import type { DataHandler, ExitHandler, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
30
+ import type { DataHandler, ExitHandler, InputChunks, ResizeHandler, TerminalHost, TerminalSize } from "../host/terminal-host.js";
31
31
  import type { Unsubscribe } from "../core/subscription.js";
32
32
  import type { Env } from "../core/env.js";
33
33
  /**
@@ -42,6 +42,12 @@ interface NodeReadable {
42
42
  resume?(): unknown;
43
43
  pause?(): unknown;
44
44
  readonly isTTY?: boolean;
45
+ /**
46
+ * Set by a stream that delivers whole keys — the docs site's simulated
47
+ * stdin forwards xterm.js's events one to one. Node's own stdin is a tty
48
+ * read and sets nothing, so it is `"split"`.
49
+ */
50
+ readonly inputChunks?: InputChunks;
45
51
  }
46
52
  interface NodeWritable {
47
53
  write(chunk: string | Uint8Array): unknown;
@@ -98,6 +104,7 @@ export declare class NodeTerminalHost implements TerminalHost {
98
104
  private readonly stdout;
99
105
  private readonly process;
100
106
  readonly env: Env;
107
+ readonly inputChunks: InputChunks;
101
108
  private readonly dataHandlers;
102
109
  private readonly resizeHandlers;
103
110
  private dataListener;
@@ -45,6 +45,7 @@ export class NodeTerminalHost {
45
45
  stdout;
46
46
  process;
47
47
  env;
48
+ inputChunks;
48
49
  dataHandlers = new Set();
49
50
  resizeHandlers = new Set();
50
51
  dataListener;
@@ -55,6 +56,7 @@ export class NodeTerminalHost {
55
56
  this.stdout = options.stdout ?? process.stdout;
56
57
  this.env = options.env ?? process.env;
57
58
  this.process = options.process ?? process;
59
+ this.inputChunks = this.stdin.inputChunks ?? "split";
58
60
  }
59
61
  get isTTY() {
60
62
  return !!this.stdin.isTTY && this.writesToTerminal;