@promptctl/rich-js 0.19.0 → 0.20.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 (53) hide show
  1. package/README.md +3 -2
  2. package/dist/core/color.d.ts +1 -0
  3. package/dist/core/color.js +3 -2
  4. package/dist/core/noise.d.ts +31 -0
  5. package/dist/core/noise.js +94 -0
  6. package/dist/core/paint.js +13 -2
  7. package/dist/core/pretty.d.ts +23 -2
  8. package/dist/core/pretty.js +54 -18
  9. package/dist/core/protocol.d.ts +2 -0
  10. package/dist/core/protocol.js +3 -2
  11. package/dist/core/selection.d.ts +12 -0
  12. package/dist/core/selection.js +14 -0
  13. package/dist/core/strip.d.ts +103 -72
  14. package/dist/core/strip.js +240 -212
  15. package/dist/core/style.js +9 -7
  16. package/dist/host/app.d.ts +68 -2
  17. package/dist/host/app.js +125 -15
  18. package/dist/host/index.d.ts +3 -1
  19. package/dist/host/index.js +1 -0
  20. package/dist/host/input.d.ts +84 -0
  21. package/dist/host/input.js +498 -0
  22. package/dist/host/terminal-host.d.ts +12 -0
  23. package/dist/host/terminal-host.js +2 -0
  24. package/dist/index.d.ts +9 -4
  25. package/dist/index.js +6 -3
  26. package/dist/node/terminal-host.d.ts +7 -0
  27. package/dist/node/terminal-host.js +7 -0
  28. package/dist/renderables/effect.d.ts +23 -8
  29. package/dist/renderables/effect.js +48 -26
  30. package/dist/renderables/effects.d.ts +252 -0
  31. package/dist/renderables/effects.js +565 -0
  32. package/dist/renderables/embed.d.ts +11 -5
  33. package/dist/renderables/embed.js +13 -6
  34. package/dist/renderables/list-view.d.ts +42 -0
  35. package/dist/renderables/list-view.js +78 -0
  36. package/dist/renderables/progress.d.ts +6 -0
  37. package/dist/renderables/progress.js +11 -4
  38. package/dist/renderables/progressBar.d.ts +22 -2
  39. package/dist/renderables/progressBar.js +51 -7
  40. package/dist/renderables/rule.d.ts +2 -2
  41. package/dist/renderables/rule.js +12 -10
  42. package/dist/renderables/table.js +5 -7
  43. package/dist/renderables/viewport.d.ts +30 -0
  44. package/dist/renderables/viewport.js +59 -3
  45. package/dist/widgets/dropdown.js +3 -3
  46. package/dist/widgets/event-router.d.ts +31 -40
  47. package/dist/widgets/event-router.js +52 -483
  48. package/dist/widgets/index.d.ts +2 -1
  49. package/dist/widgets/types.d.ts +8 -25
  50. package/dist/widgets/types.js +8 -0
  51. package/dist/widgets/widget-app.d.ts +22 -7
  52. package/dist/widgets/widget-app.js +22 -12
  53. package/package.json +22 -18
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Terminal input: the bytes a terminal sends for keys and the pointer,
3
+ * parsed into `KeyPress` and `ScreenMouseEvent` values, and the one name
4
+ * each key goes by.
5
+ *
6
+ * [LAW:single-enforcer] Escape-sequence parsing lives only here. `App` owns
7
+ * the one parser over its host's input; a `WidgetApp` routes the events `App`
8
+ * hears, and no program reads its terminal's bytes itself.
9
+ *
10
+ * [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
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.
16
+ *
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). The next chunk cancels the timer
20
+ * if it extends the sequence. `flush()` drains it synchronously.
21
+ */
22
+ /**
23
+ * The one name a key goes by — what a keymap is keyed by and what
24
+ * `quitKeys` lists. A key that types a character is named by it (`q`, `G`,
25
+ * `>`); any other is named by its key (`up`, `enter`, `space`, `f5`). Held
26
+ * modifiers come first, always in the order `ctrl+`, `meta+`, `shift+`:
27
+ * `ctrl+c`, `shift+tab`, `meta+left`. Shift is not named on a character,
28
+ * which already carries it: Shift+G is `G`.
29
+ */
30
+ export function keyName(press) {
31
+ const typed = press.character !== "" && press.key !== "space";
32
+ const modifiers = [
33
+ press.ctrl ? "ctrl+" : "",
34
+ press.meta ? "meta+" : "",
35
+ press.shift && !typed ? "shift+" : "",
36
+ ];
37
+ return modifiers.join("") + (typed ? press.character : press.key);
38
+ }
39
+ // Single-byte → key name table for the trivial cases.
40
+ const SINGLE_BYTE_KEYS = {
41
+ 0x09: "tab",
42
+ 0x0a: "enter",
43
+ 0x0d: "enter",
44
+ 0x1b: "escape",
45
+ 0x08: "backspace",
46
+ 0x7f: "backspace",
47
+ 0x20: "space",
48
+ };
49
+ // Final-byte → key name for the simple `ESC[<X>` arrow / nav forms.
50
+ const CSI_LETTER_KEYS = {
51
+ A: "up",
52
+ B: "down",
53
+ C: "right",
54
+ D: "left",
55
+ H: "home",
56
+ F: "end",
57
+ Z: "tab", // shift+tab encodes as ESC[Z
58
+ };
59
+ // `ESC[<n>~` numeric forms for navigation / function keys.
60
+ const CSI_TILDE_KEYS = {
61
+ "1": "home",
62
+ "2": "insert",
63
+ "3": "delete",
64
+ "4": "end",
65
+ "5": "pageup",
66
+ "6": "pagedown",
67
+ "7": "home",
68
+ "8": "end",
69
+ "11": "f1",
70
+ "12": "f2",
71
+ "13": "f3",
72
+ "14": "f4",
73
+ "15": "f5",
74
+ "17": "f6",
75
+ "18": "f7",
76
+ "19": "f8",
77
+ "20": "f9",
78
+ "21": "f10",
79
+ "23": "f11",
80
+ "24": "f12",
81
+ };
82
+ // SS3 form `ESC O X` for F1–F4 (and arrow keys on some terminals).
83
+ const SS3_KEYS = {
84
+ P: "f1",
85
+ Q: "f2",
86
+ R: "f3",
87
+ S: "f4",
88
+ A: "up",
89
+ B: "down",
90
+ C: "right",
91
+ D: "left",
92
+ H: "home",
93
+ F: "end",
94
+ };
95
+ // Every name a key with no glyph goes by.
96
+ const NAMED_KEYS = new Set([
97
+ ...Object.values(SINGLE_BYTE_KEYS),
98
+ ...Object.values(CSI_LETTER_KEYS),
99
+ ...Object.values(CSI_TILDE_KEYS),
100
+ ...Object.values(SS3_KEYS),
101
+ ]);
102
+ // [LAW:one-source-of-truth] The glyphs a Ctrl chord is named by are what
103
+ // `ctrlKey` makes of the control bytes no named key claims.
104
+ const CTRL_GLYPHS = new Set(Array.from({ length: 0x20 }, (_, byte) => byte)
105
+ .filter((byte) => byte !== 0 && !(byte in SINGLE_BYTE_KEYS))
106
+ .map((byte) => ctrlKey(byte).key));
107
+ /**
108
+ * `name`, if `keyName` can produce it: modifiers in its order, then a named
109
+ * key or one glyph, with no `shift+` on a glyph and, after `ctrl+`, only a
110
+ * glyph a control byte stands for.
111
+ * [LAW:parse-dont-validate] A name no key can produce throws here, at the
112
+ * one place a program hands names in, rather than binding a key that never fires.
113
+ */
114
+ export function parseKeyName(name) {
115
+ const [, ctrl, , shift, base] = /^(ctrl\+)?(meta\+)?(shift\+)?(.*)$/u.exec(name);
116
+ const glyph = [...base].length === 1 && !shift && (!ctrl || CTRL_GLYPHS.has(base));
117
+ const producible = NAMED_KEYS.has(base) || glyph;
118
+ if (!producible)
119
+ throw new Error(`no key is named ${JSON.stringify(name)}; see keyName`);
120
+ return name;
121
+ }
122
+ const ESC = 0x1b;
123
+ const LBRACKET = 0x5b; // [
124
+ const LANGLE = 0x3c; // <
125
+ const O_BYTE = 0x4f; // O
126
+ const M_UPPER = 0x4d; // M
127
+ const M_LOWER = 0x6d; // m
128
+ // [LAW:single-enforcer] Shared encode/decode singletons. The parser
129
+ // receives `Uint8Array | string` from the host and walks bytes through
130
+ // its state machine; conversion happens at the two boundaries (string
131
+ // input → bytes via TextEncoder; printable-byte run → JS string via
132
+ // TextDecoder) and only there. Module-level instances keep the seam
133
+ // allocation-free and runtime-agnostic — both APIs are universal across
134
+ // node and browser.
135
+ const UTF8_ENCODER = new TextEncoder();
136
+ const UTF8_DECODER = new TextDecoder("utf-8");
137
+ const ASCII_DECODER = new TextDecoder("ascii");
138
+ const EMPTY_BYTES = new Uint8Array(0);
139
+ // Concatenate two byte arrays into a fresh Uint8Array. Replaces
140
+ // `Buffer.concat([...])` so the parser holds no node-specific globals.
141
+ function concatBytes(a, b) {
142
+ if (a.length === 0)
143
+ return b;
144
+ if (b.length === 0)
145
+ return a;
146
+ const merged = new Uint8Array(a.length + b.length);
147
+ merged.set(a, 0);
148
+ merged.set(b, a.length);
149
+ return merged;
150
+ }
151
+ export class InputParser {
152
+ sink;
153
+ // [LAW:types-are-the-program] The parse buffer is `Uint8Array`, not
154
+ // `Buffer` — node's Buffer extends Uint8Array, so Buffer chunks from
155
+ // process.stdin satisfy this type, while a browser host can emit native
156
+ // Uint8Array without depending on a Buffer global polyfill.
157
+ buffer = EMPTY_BYTES;
158
+ // [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
159
+ // across node and browser; `setImmediate` is node-only.
160
+ escTimer;
161
+ constructor(sink) {
162
+ this.sink = sink;
163
+ }
164
+ /** Feed a chunk of bytes (or a string of them) into the parser. */
165
+ feed(chunk) {
166
+ const next = typeof chunk === "string" ? UTF8_ENCODER.encode(chunk) : chunk;
167
+ this.buffer = concatBytes(this.buffer, next);
168
+ this.cancelEscape();
169
+ this.drain();
170
+ // [LAW:dataflow-not-control-flow] If a lone ESC remains, defer emission
171
+ // until the next macrotask so a follow-up chunk can extend it.
172
+ if (this.buffer.length === 1 && this.buffer[0] === ESC) {
173
+ this.escTimer = setTimeout(() => {
174
+ this.escTimer = undefined;
175
+ this.flush();
176
+ }, 0);
177
+ }
178
+ }
179
+ /** Force any pending lone ESC out of the buffer. */
180
+ flush() {
181
+ this.cancelEscape();
182
+ if (this.buffer.length === 1 && this.buffer[0] === ESC) {
183
+ this.buffer = EMPTY_BYTES;
184
+ this.sink.key(({ key: "escape", character: "", shift: false, ctrl: false, meta: false }));
185
+ }
186
+ }
187
+ /** Drop what is buffered and any pending lone ESC: the parser as constructed. */
188
+ reset() {
189
+ this.cancelEscape();
190
+ this.buffer = EMPTY_BYTES;
191
+ }
192
+ cancelEscape() {
193
+ clearTimeout(this.escTimer);
194
+ this.escTimer = undefined;
195
+ }
196
+ drain() {
197
+ while (this.buffer.length > 0) {
198
+ const result = this.consumeOne(this.buffer);
199
+ if (result.kind === "incomplete")
200
+ return;
201
+ this.buffer = this.buffer.subarray(result.bytes);
202
+ if (result.kind === "key")
203
+ this.sink.key(result.event);
204
+ else if (result.kind === "mouse")
205
+ this.sink.mouse(result.event);
206
+ // skip → drop bytes silently
207
+ }
208
+ }
209
+ consumeOne(buf) {
210
+ const b0 = buf[0];
211
+ if (b0 === ESC)
212
+ return this.consumeEscape(buf);
213
+ // Ctrl+C
214
+ if (b0 === 0x03) {
215
+ return {
216
+ kind: "key",
217
+ bytes: 1,
218
+ event: ({ key: "c", character: "", shift: false, ctrl: true, meta: false }),
219
+ };
220
+ }
221
+ // Named single-byte keys (tab/enter/space/backspace).
222
+ const named = SINGLE_BYTE_KEYS[b0];
223
+ if (named && named !== "escape") {
224
+ return {
225
+ kind: "key",
226
+ bytes: 1,
227
+ event: ({
228
+ key: named,
229
+ character: named === "space" ? " " : "",
230
+ shift: false,
231
+ ctrl: false,
232
+ meta: false,
233
+ }),
234
+ };
235
+ }
236
+ if (b0 < 0x20)
237
+ return { kind: "key", bytes: 1, event: ctrlKey(b0) };
238
+ // Printable ASCII / start of a UTF-8 multibyte character.
239
+ return this.consumePrintable(buf);
240
+ }
241
+ consumePrintable(buf) {
242
+ const b0 = buf[0];
243
+ let len = 1;
244
+ if (b0 >= 0xc0 && b0 < 0xe0)
245
+ len = 2;
246
+ else if (b0 >= 0xe0 && b0 < 0xf0)
247
+ len = 3;
248
+ else if (b0 >= 0xf0)
249
+ len = 4;
250
+ if (buf.length < len)
251
+ return { kind: "incomplete" };
252
+ const character = UTF8_DECODER.decode(buf.subarray(0, len));
253
+ return {
254
+ kind: "key",
255
+ bytes: len,
256
+ event: ({
257
+ key: character.toLowerCase(),
258
+ character,
259
+ shift: character.length === 1 && character !== character.toLowerCase(),
260
+ ctrl: false,
261
+ meta: false,
262
+ }),
263
+ };
264
+ }
265
+ consumeEscape(buf) {
266
+ if (buf.length === 1)
267
+ return { kind: "incomplete" };
268
+ const b1 = buf[1];
269
+ // ESC [ ... → CSI
270
+ if (b1 === LBRACKET)
271
+ return this.consumeCSI(buf);
272
+ // ESC O X → SS3 (F1–F4 / arrows on some terms)
273
+ if (b1 === O_BYTE)
274
+ return this.consumeSS3(buf);
275
+ // ESC ESC [ … / ESC ESC O … → Alt on a key the terminal sends as a
276
+ // sequence (macOS Terminal and iTerm send Option+Up as `ESC ESC [ A`): the
277
+ // inner sequence, with meta held.
278
+ if (b1 === ESC && buf.length > 2 && (buf[2] === LBRACKET || buf[2] === O_BYTE)) {
279
+ // A pointer report is never Alt-prefixed (SGR carries modifiers in its
280
+ // button byte), so before anything but a key the first ESC is Escape.
281
+ const inner = this.consumeEscape(buf.subarray(1));
282
+ if (inner.kind === "incomplete")
283
+ return inner;
284
+ if (inner.kind === "key") {
285
+ return { kind: "key", bytes: inner.bytes + 1, event: { ...inner.event, meta: true } };
286
+ }
287
+ return { kind: "key", bytes: 1, event: ESCAPE };
288
+ }
289
+ // ESC ESC → escape key (some terminals double-send). Consume BOTH bytes
290
+ // so the user sees one escape event, not two. Without this, the second
291
+ // ESC remained in the buffer and produced a second escape via the
292
+ // lone-ESC flush path.
293
+ if (b1 === ESC) {
294
+ return {
295
+ kind: "key",
296
+ bytes: 2,
297
+ event: ({ key: "escape", character: "", shift: false, ctrl: false, meta: false }),
298
+ };
299
+ }
300
+ // ESC <named> / ESC <printable> → Alt-modified key.
301
+ // Terminals encode Alt+<key> as the literal byte preceded by ESC, so
302
+ // `ESC b` is Alt+B, `ESC <0x7f>` is Alt+Backspace, `ESC <0x20>` is
303
+ // Alt+Space, etc. The parser surfaces this as `{ key, meta: true }` —
304
+ // matching the shape used for Ctrl-modified keys, so handlers can
305
+ // branch on `event.meta` without parsing escape sequences themselves.
306
+ //
307
+ // [LAW:one-source-of-truth] Named single-byte keys (space, tab,
308
+ // enter, backspace) use the SAME canonical key names on the Alt path
309
+ // as on the non-Alt path. Without this, `Alt+Space` decoded as
310
+ // `key: " "` while a bare space decoded as `key: "space"`, so a
311
+ // handler couldn't match Alt+Space via `event.key === "space" &&
312
+ // event.meta`. Routing through SINGLE_BYTE_KEYS first preserves the
313
+ // canonical naming.
314
+ //
315
+ // [LAW:one-source-of-truth] Modifier decoding for *all* navigation keys
316
+ // lives in this file: CSI param-style modifiers (e.g. `ESC[1;3D` for
317
+ // Alt+Left) flow through `decodeModifier`, and ESC-prefixed Alt forms
318
+ // flow through this branch. Widgets read `event.meta` and never re-parse.
319
+ const namedAlt = SINGLE_BYTE_KEYS[b1];
320
+ if (namedAlt && namedAlt !== "escape") {
321
+ return {
322
+ kind: "key",
323
+ bytes: 2,
324
+ event: ({
325
+ key: namedAlt,
326
+ character: "",
327
+ shift: false,
328
+ ctrl: false,
329
+ meta: true,
330
+ }),
331
+ };
332
+ }
333
+ // The key is the glyph the terminal sent, as for Alt+`>`: the glyph
334
+ // already carries shift, so Alt+G is `meta+G` and never `meta+shift+g`.
335
+ if (b1 >= 0x20 && b1 <= 0x7e) {
336
+ const ch = String.fromCharCode(b1);
337
+ return {
338
+ kind: "key",
339
+ bytes: 2,
340
+ event: ({ key: ch, character: "", shift: false, ctrl: false, meta: true }),
341
+ };
342
+ }
343
+ // ESC <control byte> is read as Escape, then the Ctrl chord — never as
344
+ // Alt+Ctrl. The bytes cannot tell the two apart, and Escape then Ctrl+C
345
+ // typed during a stalled frame must still reach the quit keys as Ctrl+C.
346
+ // Any other byte after ESC (a UTF-8 lead) is likewise re-read on its own.
347
+ return {
348
+ kind: "key",
349
+ bytes: 1,
350
+ event: ({ key: "escape", character: "", shift: false, ctrl: false, meta: false }),
351
+ };
352
+ }
353
+ consumeSS3(buf) {
354
+ if (buf.length < 3)
355
+ return { kind: "incomplete" };
356
+ const final = String.fromCharCode(buf[2]);
357
+ const name = SS3_KEYS[final];
358
+ if (!name)
359
+ return { kind: "skip", bytes: 3 };
360
+ return {
361
+ kind: "key",
362
+ bytes: 3,
363
+ event: ({ key: name, character: "", shift: false, ctrl: false, meta: false }),
364
+ };
365
+ }
366
+ consumeCSI(buf) {
367
+ // buf starts with ESC [
368
+ const next = buf[2];
369
+ if (next === undefined)
370
+ return { kind: "incomplete" };
371
+ // X10 mouse: ESC [ M b x y (3 raw bytes after M)
372
+ if (next === M_UPPER) {
373
+ if (buf.length < 6)
374
+ return { kind: "incomplete" };
375
+ const cb = buf[3] - 32;
376
+ const col = buf[4] - 32 - 1;
377
+ const row = buf[5] - 32 - 1;
378
+ return { kind: "mouse", bytes: 6, event: decodeMouseFromCb(cb, col, row, true) };
379
+ }
380
+ // SGR mouse: ESC [ < cb ; x ; y M|m
381
+ if (next === LANGLE) {
382
+ // Find terminator M or m.
383
+ for (let i = 3; i < buf.length; i++) {
384
+ const b = buf[i];
385
+ if (b === M_UPPER || b === M_LOWER) {
386
+ const params = ASCII_DECODER.decode(buf.subarray(3, i));
387
+ const parts = params.split(";");
388
+ if (parts.length !== 3)
389
+ return { kind: "skip", bytes: i + 1 };
390
+ const cb = parseInt(parts[0], 10);
391
+ const col = parseInt(parts[1], 10) - 1;
392
+ const row = parseInt(parts[2], 10) - 1;
393
+ if (Number.isNaN(cb) || Number.isNaN(col) || Number.isNaN(row)) {
394
+ return { kind: "skip", bytes: i + 1 };
395
+ }
396
+ return {
397
+ kind: "mouse",
398
+ bytes: i + 1,
399
+ event: decodeMouseFromCb(cb, col, row, b === M_UPPER),
400
+ };
401
+ }
402
+ }
403
+ return { kind: "incomplete" };
404
+ }
405
+ // Generic CSI: collect parameter / intermediate bytes until a final byte
406
+ // in the range 0x40–0x7e is found.
407
+ let params = "";
408
+ for (let i = 2; i < buf.length; i++) {
409
+ const b = buf[i];
410
+ if (b >= 0x40 && b <= 0x7e) {
411
+ const final = String.fromCharCode(b);
412
+ return decodeCSI(params, final, i + 1);
413
+ }
414
+ params += String.fromCharCode(b);
415
+ }
416
+ return { kind: "incomplete" };
417
+ }
418
+ }
419
+ // --- Helpers ---
420
+ // The wheel's button bits: vertical, then the horizontal wheel or tilt.
421
+ const SCROLL = ["scroll_up", "scroll_down", "scroll_left", "scroll_right"];
422
+ // A C0 control byte as the Ctrl chord that types it, with Alt held or not.
423
+ // 0x00 is Ctrl+Space; 0x01–0x1a are Ctrl+A–Z; 0x1c–0x1f are Ctrl+\ ] ^ _.
424
+ // The bytes SINGLE_BYTE_KEYS names (tab, enter, backspace, escape) never reach here.
425
+ function ctrlKey(byte) {
426
+ const key = byte === 0 ? "space" : String.fromCharCode(byte + (byte <= 0x1a ? 0x60 : 0x40));
427
+ return { key, character: "", shift: false, ctrl: true, meta: false };
428
+ }
429
+ const ESCAPE = { key: "escape", character: "", shift: false, ctrl: false, meta: false };
430
+ function decodeMouseFromCb(cb, x, y, isPress) {
431
+ const isMotion = (cb & 32) !== 0;
432
+ const isScroll = (cb & 64) !== 0;
433
+ const button = cb & 3;
434
+ const shift = !!(cb & 4);
435
+ const ctrl = !!(cb & 16); // bit 16 = ctrl in xterm encoding
436
+ if (isScroll)
437
+ return { type: SCROLL[button], x, y, button: 0, shift, ctrl };
438
+ if (isMotion) {
439
+ return { type: "mouse_move", x, y, button, shift, ctrl };
440
+ }
441
+ return {
442
+ type: isPress ? "mouse_down" : "mouse_up",
443
+ x,
444
+ y,
445
+ button,
446
+ shift,
447
+ ctrl,
448
+ };
449
+ }
450
+ function decodeCSI(params, final, bytes) {
451
+ // Handle modifier suffixes like `1;2A` (shift+up). We split params on `;`.
452
+ const parts = params.split(";");
453
+ const first = parts[0] ?? "";
454
+ const mod = parseInt(parts[1] ?? "", 10);
455
+ const { shift, ctrl, meta } = decodeModifier(mod);
456
+ if (final === "~") {
457
+ const name = CSI_TILDE_KEYS[first];
458
+ if (!name)
459
+ return { kind: "skip", bytes };
460
+ return {
461
+ kind: "key",
462
+ bytes,
463
+ event: ({ key: name, character: "", shift, ctrl, meta }),
464
+ };
465
+ }
466
+ const name = CSI_LETTER_KEYS[final];
467
+ if (!name)
468
+ return { kind: "skip", bytes };
469
+ // Special case: ESC[Z is shift+tab regardless of explicit modifier.
470
+ // [LAW:one-source-of-truth] `character` is the printable character the
471
+ // key would produce, or "" for non-printable / modifier-only events.
472
+ // The non-shifted tab path emits character: "" (Tab is named, not
473
+ // printed) — keep shift+tab consistent. Distinguish via key === "tab"
474
+ // + shift, not character.
475
+ if (final === "Z") {
476
+ return {
477
+ kind: "key",
478
+ bytes,
479
+ event: ({ key: "tab", character: "", shift: true, ctrl: false, meta: false }),
480
+ };
481
+ }
482
+ return {
483
+ kind: "key",
484
+ bytes,
485
+ event: ({ key: name, character: "", shift, ctrl, meta }),
486
+ };
487
+ }
488
+ function decodeModifier(mod) {
489
+ if (!Number.isFinite(mod) || mod <= 1) {
490
+ return { shift: false, ctrl: false, meta: false };
491
+ }
492
+ const bits = mod - 1;
493
+ return {
494
+ shift: (bits & 1) !== 0,
495
+ meta: (bits & 2) !== 0,
496
+ ctrl: (bits & 4) !== 0,
497
+ };
498
+ }
@@ -92,6 +92,12 @@ export interface TerminalHost {
92
92
  * share a runtime with, which in a browser is none.
93
93
  */
94
94
  readonly env: Env;
95
+ /**
96
+ * Scroll whatever holds this terminal by `lines` (positive towards the end
97
+ * of its content): a wheel turn that moved nothing in the program, passed
98
+ * outward. A terminal that is the outermost thing does nothing with it.
99
+ */
100
+ passWheel(lines: number): void;
95
101
  /**
96
102
  * Run `handler` once, when the program ends by any path this host can
97
103
  * observe, so whoever changed the terminal can hand it back. The program
@@ -157,6 +163,11 @@ export interface BrowserTerminalHostOptions {
157
163
  * `NodeTerminalHost`'s relationship to `process.stdin`/`process.stdout`.
158
164
  */
159
165
  terminal: XtermTerminal;
166
+ /**
167
+ * Scroll the page by `lines` of this terminal's rows: what the host does
168
+ * with a wheel turn the program passed outward (`TerminalHost.passWheel`).
169
+ */
170
+ passWheel: (lines: number) => void;
160
171
  }
161
172
  export declare class BrowserTerminalHost implements TerminalHost {
162
173
  private readonly terminal;
@@ -164,6 +175,7 @@ export declare class BrowserTerminalHost implements TerminalHost {
164
175
  private readonly resizeHandlers;
165
176
  private dataSubscription;
166
177
  private resizeSubscription;
178
+ readonly passWheel: (lines: number) => void;
167
179
  constructor(options: BrowserTerminalHostOptions);
168
180
  get isTTY(): boolean;
169
181
  get writesToTerminal(): boolean;
@@ -46,8 +46,10 @@ export class BrowserTerminalHost {
46
46
  resizeHandlers = new Set();
47
47
  dataSubscription;
48
48
  resizeSubscription;
49
+ passWheel;
49
50
  constructor(options) {
50
51
  this.terminal = options.terminal;
52
+ this.passWheel = options.passWheel;
51
53
  }
52
54
  // [LAW:dataflow-not-control-flow] xterm.js is always an interactive
53
55
  // terminal — there is no "non-TTY" code path in the browser. The
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { cellLen, setCellSize, splitText, chopCells, cellFit, asCellCol, asCodePoint, } from "./core/cells.js";
1
+ export { cellLen, setCellSize, splitText, chopCells, cellFit, graphemes, asCellCol, asCodePoint, } from "./core/cells.js";
2
2
  export type { CellCol, CodeUnit, CodePoint } from "./core/cells.js";
3
3
  export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
4
  export type { ColorSystemName, DetectColorOptions, Destination } from "./core/color.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, PowerlineJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER_GLYPHS, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
37
- export type { StyledRenderable, Joiner, PowerlineJoinerOptions, CapsuleJoinerOptions, PlainJoinerOptions, GradientJoinerOptions, } from "./core/strip.js";
36
+ export { Strip, CellJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER, CAPSULE_JOINER, PLAIN_JOINER, gradientJoiner, } from "./core/strip.js";
37
+ export type { StyledRenderable, Joiner, JoinShape, JoinCell, CellColor, Paint } 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";
@@ -57,11 +57,16 @@ export type { Env } from "./core/env.js";
57
57
  export { Constrain } from "./renderables/constrain.js";
58
58
  export { Effected } from "./renderables/effect.js";
59
59
  export type { CellColors, Effect, EffectCell, EffectedOptions } from "./renderables/effect.js";
60
+ export { EFFECT_CURVES, EFFECT_LIGHTS, SHIMMER_WIDTH, dissolveOut, fadeIn, onColors, pulse, shares, shimmer, sparkle, wheel, } from "./renderables/effects.js";
61
+ export type { Curve, Field, Loop, Pair, Touch, Transition } from "./renderables/effects.js";
60
62
  export { Align } from "./renderables/align.js";
61
63
  export type { Alignment } from "./core/place.js";
62
64
  export { Padding } from "./renderables/padding.js";
63
65
  export type { PaddingDimensions } from "./renderables/padding.js";
64
- export { Viewport, SCROLLBAR } from "./renderables/viewport.js";
66
+ export { Viewport, ViewportPerKey, SCROLLBAR } from "./renderables/viewport.js";
67
+ export { ListView } from "./renderables/list-view.js";
68
+ export { moveSelection } from "./core/selection.js";
69
+ export type { ListViewOptions } from "./renderables/list-view.js";
65
70
  export type { Scrollbar, ScrollbarPart, ViewportOptions } from "./renderables/viewport.js";
66
71
  export { Rule } from "./renderables/rule.js";
67
72
  export type { RuleAlign, RuleOptions } from "./renderables/rule.js";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Core primitives
2
- export { cellLen, setCellSize, splitText, chopCells, cellFit, asCellCol, asCodePoint, } from "./core/cells.js";
2
+ export { cellLen, setCellSize, splitText, chopCells, cellFit, graphemes, asCellCol, asCodePoint, } from "./core/cells.js";
3
3
  export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, parseHexColor, blendRgb, resolveColorSystem, detectColorSystem, resolveDestination, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
4
4
  // Perceptually-uniform color space (manipulation, transposition).
5
5
  export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.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, PowerlineJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER_GLYPHS, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
47
+ export { Strip, CellJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER, CAPSULE_JOINER, PLAIN_JOINER, gradientJoiner, } 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
@@ -70,9 +70,12 @@ export { Console } from "./core/console.js";
70
70
  // Renderables
71
71
  export { Constrain } from "./renderables/constrain.js";
72
72
  export { Effected } from "./renderables/effect.js";
73
+ export { EFFECT_CURVES, EFFECT_LIGHTS, SHIMMER_WIDTH, dissolveOut, fadeIn, onColors, pulse, shares, shimmer, sparkle, wheel, } from "./renderables/effects.js";
73
74
  export { Align } from "./renderables/align.js";
74
75
  export { Padding } from "./renderables/padding.js";
75
- export { Viewport, SCROLLBAR } from "./renderables/viewport.js";
76
+ export { Viewport, ViewportPerKey, SCROLLBAR } from "./renderables/viewport.js";
77
+ export { ListView } from "./renderables/list-view.js";
78
+ export { moveSelection } from "./core/selection.js";
76
79
  export { Rule } from "./renderables/rule.js";
77
80
  export { Panel } from "./renderables/panel.js";
78
81
  export { Group } from "./renderables/group.js";
@@ -50,6 +50,12 @@ interface NodeWritable {
50
50
  readonly columns?: number;
51
51
  readonly rows?: number;
52
52
  readonly isTTY?: boolean;
53
+ /**
54
+ * Scroll whatever holds the terminal this stream writes to. Node's own
55
+ * stdout has none: its terminal is the outermost thing. A stand-in whose
56
+ * terminal sits in a page (the docs' example card) has one.
57
+ */
58
+ passWheel?(lines: number): void;
53
59
  }
54
60
  /**
55
61
  * Subset of node's `process` the host hears the program end through and
@@ -101,6 +107,7 @@ export declare class NodeTerminalHost implements TerminalHost {
101
107
  get isTTY(): boolean;
102
108
  get writesToTerminal(): boolean;
103
109
  write(data: Uint8Array | string): void;
110
+ passWheel(lines: number): void;
104
111
  size(): TerminalSize;
105
112
  setRawMode(raw: boolean): void;
106
113
  private applyRawMode;
@@ -65,6 +65,13 @@ export class NodeTerminalHost {
65
65
  write(data) {
66
66
  this.stdout.write(data);
67
67
  }
68
+ // A terminal that reports the wheel to the program has taken it from its
69
+ // own scrollback, and nothing here can hand it back: on node's stdout this
70
+ // does nothing. A stdout whose terminal sits in something that scrolls
71
+ // carries the way out itself.
72
+ passWheel(lines) {
73
+ this.stdout.passWheel?.(lines);
74
+ }
68
75
  size() {
69
76
  return {
70
77
  cols: this.stdout.columns ?? DEFAULT_COLS,