@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
package/README.md CHANGED
@@ -387,9 +387,10 @@ npm run demo
387
387
  | `npm run template-bindings` | rich-template-bindings | Type a template on the left and watch it render on the right. |
388
388
  | `npm run themes-and-color-studio` | themes-and-color-studio | A printed tour of colours, palettes, bundled themes, and contrast. Prints once and exits; set any of `EXPORT_HTML=out.html`, `EXPORT_SVG=out.svg` and `EXPORT_TEXT=out.txt` to also save it to each file named. |
389
389
  | `npm run strip` | rich-strip | Every built-in `Joiner`, printed side by side. Prints once and exits. |
390
- | `npm run effects-feel` | effects-feel | Shimmer, pulse, drift, sparkle, fade-in and dissolve-out on a powerline strip and a run of text, at a frame rate, colour depth and ground you choose. A demo for agreeing how the effects feel; `npm run effects-feel -- --help` lists the flags. |
390
+ | `npm run effects-feel` | effects-feel | Shimmer, a gentle pulse on chosen elements, sparkle, a hue wheel, fade-in and a dissolve-out that eddies back before it goes, on a powerline strip and a run of text, at a colour depth you choose, with keys to step the frame rate, the demo's pace and the theme while it runs. A demo for agreeing how the effects feel; `npm run effects-feel -- --help` lists the flags and keys. |
391
391
  | `npm run markup-plugins` | rich-markup-plugins | Custom markup tags registered through `MarkupRegistry`. Prints once and exits. |
392
- | `npm run viewport` | rich-viewport | A cursor walking a list taller than its window, kept in view by `Viewport.ensureVisible`. |
392
+ | `npm run viewport` | rich-viewport | A selection you move through a list taller than its window with the arrow keys, `j`/`k`, PgUp/PgDn and Home/End, kept in view by `Viewport.ensureVisible`; the mouse wheel scrolls the window, and `q` quits. |
393
+ | `npm run terminal-host` | terminal-host | Each key you press printed as the bytes the terminal sends for it, and each resize as the new size — a program written against `TerminalHost` alone. `examples/terminal-host/embed.ts` runs the same program on an xterm.js terminal in your own page. |
393
394
 
394
395
  `Ctrl-C` quits any demo that stays running.
395
396
 
@@ -58,6 +58,7 @@ export declare function relativeLuminance(c: ColorRgba): number;
58
58
  * foreground, flatten it first (or use `ensureContrast`, which does).
59
59
  */
60
60
  export declare function contrastRatio(a: ColorRgba, b: ColorRgba): number;
61
+ export declare function rgbDistance(a: ColorRgba, b: ColorRgba): number;
61
62
  /**
62
63
  * An indexed palette: entry `i` of `colors` is terminal index `firstIndex + i`.
63
64
  * `firstIndex` lets a table hold only the part of a palette a downgrade may
@@ -113,8 +113,9 @@ function luminanceRatio(la, lb) {
113
113
  }
114
114
  // --- ColorTable ---
115
115
  // [LAW:one-source-of-truth] The one distance every table scan ranks by, so
116
- // `matchWhere`'s "nearest by the distance `match` uses" cannot drift.
117
- function rgbDistance(a, b) {
116
+ // `matchWhere`'s "nearest by the distance `match` uses" cannot drift, and
117
+ // `Effected` weighs a written colour against `ColorSpec.matchOn` by it too.
118
+ export function rgbDistance(a, b) {
118
119
  const dr = a.red - b.red;
119
120
  const dg = a.green - b.green;
120
121
  const db = a.blue - b.blue;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Gradient noise, the texture every effect in `renderables/effects` is drawn
3
+ * from. Ken Perlin's improved noise (2002): a smooth pseudo-random
4
+ * field over space and time, continuous in its first and second derivatives,
5
+ * so a cell sampled a moment later has moved a little, never jumped. Things
6
+ * the eye knows as alive — water, wind, breath, a glow — vary like this:
7
+ * coherent nearby, uncorrelated far apart, never repeating exactly.
8
+ *
9
+ * [LAW:no-ambient-temporal-coupling] A pure function of its coordinates; time
10
+ * is one of them. The permutation is fixed, so a moment draws the same frame
11
+ * on every run.
12
+ *
13
+ * Tier 0 of `src/core/`: imports nothing. Not part of the package's surface;
14
+ * the effects playground reaches it as source.
15
+ */
16
+ /** Noise at (x, y, z), in about [-1, 1]; zero at every integer lattice point. */
17
+ export declare function noise(x: number, y: number, z: number): number;
18
+ /**
19
+ * Fractal Brownian motion: `octaves` layers of noise, each twice the
20
+ * frequency and half the amplitude of the last, scaled back into about
21
+ * [-1, 1]. One layer is a soft swell; three is a swell with detail on it.
22
+ */
23
+ export declare function fbm(x: number, y: number, z: number, octaves: number): number;
24
+ /**
25
+ * A pseudo-random number in [0, 1) for the `n`th of something — a breath, a
26
+ * gust — uncorrelated with the `n ± 1`th, where `noise` would make
27
+ * neighbours alike. The classic shader hash: the fraction of a large sine.
28
+ */
29
+ export declare function hash(n: number, z: number): number;
30
+ /** Hermite smoothstep from `a` to `b`, clamped to [0, 1]. */
31
+ export declare function smoothstep(a: number, b: number, x: number): number;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Gradient noise, the texture every effect in `renderables/effects` is drawn
3
+ * from. Ken Perlin's improved noise (2002): a smooth pseudo-random
4
+ * field over space and time, continuous in its first and second derivatives,
5
+ * so a cell sampled a moment later has moved a little, never jumped. Things
6
+ * the eye knows as alive — water, wind, breath, a glow — vary like this:
7
+ * coherent nearby, uncorrelated far apart, never repeating exactly.
8
+ *
9
+ * [LAW:no-ambient-temporal-coupling] A pure function of its coordinates; time
10
+ * is one of them. The permutation is fixed, so a moment draws the same frame
11
+ * on every run.
12
+ *
13
+ * Tier 0 of `src/core/`: imports nothing. Not part of the package's surface;
14
+ * the effects playground reaches it as source.
15
+ */
16
+ /** Ken Perlin's reference permutation, doubled so a lookup never wraps. */
17
+ const P = (() => {
18
+ const base = [
19
+ 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10,
20
+ 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87,
21
+ 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211,
22
+ 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208,
23
+ 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5,
24
+ 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, 119,
25
+ 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232,
26
+ 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249,
27
+ 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205,
28
+ 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180,
29
+ ];
30
+ return [...base, ...base];
31
+ })();
32
+ /** Quintic smoothstep: zero first and second derivative at 0 and 1. */
33
+ const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
34
+ const lerp = (a, b, t) => a + t * (b - a);
35
+ /** The dot of one of twelve edge gradients, picked by `hash`, with (x, y, z). */
36
+ function grad(hash, x, y, z) {
37
+ const h = hash & 15;
38
+ const u = h < 8 ? x : y;
39
+ const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
40
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
41
+ }
42
+ /** Noise at (x, y, z), in about [-1, 1]; zero at every integer lattice point. */
43
+ export function noise(x, y, z) {
44
+ const fx = Math.floor(x);
45
+ const fy = Math.floor(y);
46
+ const fz = Math.floor(z);
47
+ const X = fx & 255;
48
+ const Y = fy & 255;
49
+ const Z = fz & 255;
50
+ x -= fx;
51
+ y -= fy;
52
+ z -= fz;
53
+ const u = fade(x);
54
+ const v = fade(y);
55
+ const w = fade(z);
56
+ const A = P[X] + Y;
57
+ const AA = P[A] + Z;
58
+ const AB = P[A + 1] + Z;
59
+ const B = P[X + 1] + Y;
60
+ const BA = P[B] + Z;
61
+ const BB = P[B + 1] + Z;
62
+ return lerp(lerp(lerp(grad(P[AA], x, y, z), grad(P[BA], x - 1, y, z), u), lerp(grad(P[AB], x, y - 1, z), grad(P[BB], x - 1, y - 1, z), u), v), lerp(lerp(grad(P[AA + 1], x, y, z - 1), grad(P[BA + 1], x - 1, y, z - 1), u), lerp(grad(P[AB + 1], x, y - 1, z - 1), grad(P[BB + 1], x - 1, y - 1, z - 1), u), v), w);
63
+ }
64
+ /**
65
+ * Fractal Brownian motion: `octaves` layers of noise, each twice the
66
+ * frequency and half the amplitude of the last, scaled back into about
67
+ * [-1, 1]. One layer is a soft swell; three is a swell with detail on it.
68
+ */
69
+ export function fbm(x, y, z, octaves) {
70
+ let sum = 0;
71
+ let amplitude = 1;
72
+ let total = 0;
73
+ for (let i = 0; i < octaves; i++) {
74
+ const f = 2 ** i;
75
+ sum += amplitude * noise(x * f, y * f, z * f);
76
+ total += amplitude;
77
+ amplitude /= 2;
78
+ }
79
+ return sum / total;
80
+ }
81
+ /**
82
+ * A pseudo-random number in [0, 1) for the `n`th of something — a breath, a
83
+ * gust — uncorrelated with the `n ± 1`th, where `noise` would make
84
+ * neighbours alike. The classic shader hash: the fraction of a large sine.
85
+ */
86
+ export function hash(n, z) {
87
+ const x = Math.sin(n * 12.9898 + z * 78.233) * 43758.5453;
88
+ return x - Math.floor(x);
89
+ }
90
+ /** Hermite smoothstep from `a` to `b`, clamped to [0, 1]. */
91
+ export function smoothstep(a, b, x) {
92
+ const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
93
+ return t * t * (3 - 2 * t);
94
+ }
@@ -47,6 +47,15 @@ const GEOMETRY = {
47
47
  // frames. The erase leads its row: after a row that fills the width, the
48
48
  // cursor sits on its last cell, and an erase there would take it.
49
49
  const ROW_START = "\r\x1b[2K";
50
+ // A frame's bytes between these two are shown at once: a terminal that has
51
+ // synchronized output (DEC private mode 2026 — iTerm2, kitty, WezTerm,
52
+ // Ghostty, foot, Windows Terminal …) holds the screen until the frame has
53
+ // fully arrived, so a repaint can never show a row erased and not yet
54
+ // redrawn. Thirty full-screen repaints a second show exactly that on a
55
+ // terminal that paints whenever bytes land, as a flicker over the whole
56
+ // frame. A terminal without the mode ignores both sequences.
57
+ const SYNC_START = "\x1b[?2026h";
58
+ const SYNC_END = "\x1b[?2026l";
50
59
  const HIDE_CURSOR = "\x1b[?25l";
51
60
  const SHOW_CURSOR = "\x1b[?25h";
52
61
  const RESET_STYLE = "\x1b[0m";
@@ -79,7 +88,7 @@ export class SurfacePainter {
79
88
  */
80
89
  paint(frame, screen, destination) {
81
90
  const rows = fitRows(frame(), screen);
82
- this.write(this.over(rows, screen, destination));
91
+ this.write(SYNC_START + this.over(rows, screen, destination) + SYNC_END);
83
92
  return rows;
84
93
  }
85
94
  /**
@@ -93,7 +102,9 @@ export class SurfacePainter {
93
102
  const rows = fitRows(frame(), screen);
94
103
  const erase = this.over([], screen, destination);
95
104
  const ended = text.endsWith("\n") ? text : `${text}\n`;
96
- return erase + ended + this.over(rows, screen, destination);
105
+ // One synchronized block around all three, so the terminal is released
106
+ // only once the frame is back.
107
+ return SYNC_START + erase + ended + this.over(rows, screen, destination) + SYNC_END;
97
108
  }
98
109
  // The bytes that paint `rows` over the last frame. They go out in one
99
110
  // string, so whatever takes the last frame away arrives with the new one.
@@ -120,12 +120,33 @@ export declare class Pretty implements Renderable, Measurable {
120
120
  * traversals.
121
121
  */
122
122
  private _scalar;
123
+ /**
124
+ * `value` quoted, and cut to `maxString` when it is longer.
125
+ *
126
+ * The dropped count is an annotation about the value, not part of it, so it
127
+ * goes after the closing quote — the way `maxLength`'s `... +N` sits beside
128
+ * the kept entries rather than inside the last one. Inside the quotes it
129
+ * reads as content, and copying it out yields a string the program never
130
+ * held.
131
+ *
132
+ * A cluster is at least one code unit, so a string no longer than the cap in
133
+ * code units is within it in clusters, and needs no segmenting.
134
+ */
135
+ private _string;
136
+ /**
137
+ * A key as an object literal would spell it: bare when it is a name or an
138
+ * index, which reads as itself, and quoted as a string otherwise. Rich reprs
139
+ * every key; printed raw, `"a b"` was indistinguishable from two words and a
140
+ * `\r` in one was dropped by `RichText`, so the key shown was not the key held.
141
+ * Quoting only what needs it keeps the JavaScript reading `{ a: 1 }`. A
142
+ * quoted key is a string and `maxString` cuts it; a bare one is a name.
143
+ */
144
+ private _key;
123
145
  /**
124
146
  * `value` cut to `max` grapheme clusters, with the rest counted after the
125
147
  * closing quote.
126
148
  *
127
- * The unit is the cluster of the value, counted before `JSON.stringify`
128
- * escapes it. A code unit cut can keep half a surrogate pair, which
149
+ * The unit is the cluster of the value, counted before `quoted` escapes it. A code unit cut can keep half a surrogate pair, which
129
150
  * JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
130
151
  * a flag or a ZWJ family. Either way the kept prefix shows something the
131
152
  * value never held, and the dropped count is in a unit nobody can see.
@@ -447,6 +447,26 @@ const SEPARATOR = ", ";
447
447
  const EXPAND_SEPARATOR = SEPARATOR.trimEnd();
448
448
  /** The marker for positions the bound dropped, or nothing when it dropped none. */
449
449
  const elided = (dropped) => dropped > 0 ? [{ head: `... +${dropped}`, join: "", holes: [] }] : [];
450
+ /**
451
+ * A key `Pretty._key` may leave bare. The bare form refuses a default-ignorable code point, which prints as
452
+ * nothing, so `"a\ufe0f"` cannot pass for `a`; `ID_Continue` admits several.
453
+ * An index is at most 15 digits: every such literal is a safe integer and
454
+ * names itself, where `99999999999999999999` would name `1e20`'s key.
455
+ */
456
+ const BARE_KEY = /^(?![^]*\p{Default_Ignorable_Code_Point})(?:[\p{ID_Start}$_][\p{ID_Continue}$]*|0|[1-9]\d{0,14})$/u;
457
+ /**
458
+ * What Python's `str.isprintable` refuses \u2014 control, format, private-use,
459
+ * unassigned, and every separator but the space \u2014 which Rich's repr escapes
460
+ * and JSON.stringify leaves raw past C0. Raw, DEL vanishes in `RichText`, a C1
461
+ * byte reaches the terminal as a control, and a format character hides or
462
+ * reorders what is around it.
463
+ */
464
+ const UNPRINTABLE = /(?! )[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Zl}\p{Zp}\p{Zs}]/gu;
465
+ /** `value` in double quotes, every character that does not print escaped. */
466
+ const quoted = (value) => JSON.stringify(value).replace(UNPRINTABLE, (c) => {
467
+ const hex = c.codePointAt(0).toString(16).padStart(4, "0");
468
+ return hex.length > 4 ? `\\u{${hex}}` : `\\u${hex}`;
469
+ });
450
470
  export class Pretty {
451
471
  data;
452
472
  indent;
@@ -520,19 +540,8 @@ export class Pretty {
520
540
  if (value === undefined)
521
541
  return "undefined";
522
542
  switch (typeof value) {
523
- case "string": {
524
- // The dropped count is an annotation about the value, not part of it,
525
- // so it goes after the closing quote — the way `maxLength`'s `... +N`
526
- // sits beside the kept entries rather than inside the last one. Inside
527
- // the quotes it reads as content, and copying it out yields a string
528
- // the program never held.
529
- //
530
- // A cluster is at least one code unit, so a string no longer than the
531
- // cap in code units is within it in clusters, and needs no segmenting.
532
- if (this.maxString === undefined || value.length <= this.maxString)
533
- return JSON.stringify(value);
534
- return this._cutString(value, this.maxString);
535
- }
543
+ case "string":
544
+ return this._string(value);
536
545
  case "number":
537
546
  case "bigint":
538
547
  case "boolean":
@@ -544,12 +553,39 @@ export class Pretty {
544
553
  }
545
554
  return null;
546
555
  }
556
+ /**
557
+ * `value` quoted, and cut to `maxString` when it is longer.
558
+ *
559
+ * The dropped count is an annotation about the value, not part of it, so it
560
+ * goes after the closing quote — the way `maxLength`'s `... +N` sits beside
561
+ * the kept entries rather than inside the last one. Inside the quotes it
562
+ * reads as content, and copying it out yields a string the program never
563
+ * held.
564
+ *
565
+ * A cluster is at least one code unit, so a string no longer than the cap in
566
+ * code units is within it in clusters, and needs no segmenting.
567
+ */
568
+ _string(value) {
569
+ if (this.maxString === undefined || value.length <= this.maxString)
570
+ return quoted(value);
571
+ return this._cutString(value, this.maxString);
572
+ }
573
+ /**
574
+ * A key as an object literal would spell it: bare when it is a name or an
575
+ * index, which reads as itself, and quoted as a string otherwise. Rich reprs
576
+ * every key; printed raw, `"a b"` was indistinguishable from two words and a
577
+ * `\r` in one was dropped by `RichText`, so the key shown was not the key held.
578
+ * Quoting only what needs it keeps the JavaScript reading `{ a: 1 }`. A
579
+ * quoted key is a string and `maxString` cuts it; a bare one is a name.
580
+ */
581
+ _key(key) {
582
+ return BARE_KEY.test(key) ? key : this._string(key);
583
+ }
547
584
  /**
548
585
  * `value` cut to `max` grapheme clusters, with the rest counted after the
549
586
  * closing quote.
550
587
  *
551
- * The unit is the cluster of the value, counted before `JSON.stringify`
552
- * escapes it. A code unit cut can keep half a surrogate pair, which
588
+ * The unit is the cluster of the value, counted before `quoted` escapes it. A code unit cut can keep half a surrogate pair, which
553
589
  * JSON.stringify prints as a `\ud83d` escape; a code point cut can keep half
554
590
  * a flag or a ZWJ family. Either way the kept prefix shows something the
555
591
  * value never held, and the dropped count is in a unit nobody can see.
@@ -563,8 +599,8 @@ export class Pretty {
563
599
  if (cut === undefined) {
564
600
  const clusters = graphemes(value);
565
601
  cut = clusters.length <= max
566
- ? JSON.stringify(value)
567
- : JSON.stringify(clusters.slice(0, max).join("")) + `+${clusters.length - max}`;
602
+ ? quoted(value)
603
+ : quoted(clusters.slice(0, max).join("")) + `+${clusters.length - max}`;
568
604
  this.cutStrings.set(value, cut);
569
605
  }
570
606
  return cut;
@@ -640,7 +676,7 @@ export class Pretty {
640
676
  const { record } = form;
641
677
  const keys = Object.keys(record);
642
678
  return this._container("{", "}", " ", keys.length, level, () => keys.slice(0, cap).map((k) => ({
643
- head: k,
679
+ head: this._key(k),
644
680
  join: ": ",
645
681
  holes: [{ read: () => record[k], tail: flat("") }],
646
682
  })));
@@ -96,6 +96,8 @@ export interface RenderOptions {
96
96
  */
97
97
  onDraw?: (owner: object) => void;
98
98
  }
99
+ /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
100
+ export declare const isAscii: (text: string) => boolean;
99
101
  /**
100
102
  * `chosen` as this output can draw it: `ascii` in its place when the output is
101
103
  * ASCII-only and `chosen` spends a glyph outside ASCII, otherwise `chosen`
@@ -4,9 +4,10 @@
4
4
  */
5
5
  import { cellCount } from "./cells.js";
6
6
  import { DEFAULT_THEME } from "./style.js";
7
- const ASCII_TEXT = /^[\x00-\x7F]*$/;
7
+ /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
8
+ export const isAscii = (text) => /^[\x00-\x7F]*$/.test(text);
8
9
  export function drawable(options, chosen, ascii, glyphs = String) {
9
- return options.asciiOnly === true && !ASCII_TEXT.test(glyphs(chosen)) ? ascii : chosen;
10
+ return options.asciiOnly === true && !isAscii(glyphs(chosen)) ? ascii : chosen;
10
11
  }
11
12
  /**
12
13
  * The budget a renderable hands the one child filling its space: its own, less
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The list cursor: one clamp, shared by every list that moves a selection —
3
+ * `ListView`'s callers and `Dropdown` alike — so it has one definition.
4
+ */
5
+ /**
6
+ * The selection `delta` items on from `index` in a list of `length`, held to
7
+ * the list's ends: `-Infinity` is the first item and `Infinity` the last. A
8
+ * move that cannot go anywhere returns `index` itself, so `next === index`
9
+ * says nothing moved and the caller can skip a repaint. An empty list has
10
+ * nowhere to move to and returns `index`.
11
+ */
12
+ export declare function moveSelection(index: number, delta: number, length: number): number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The list cursor: one clamp, shared by every list that moves a selection —
3
+ * `ListView`'s callers and `Dropdown` alike — so it has one definition.
4
+ */
5
+ /**
6
+ * The selection `delta` items on from `index` in a list of `length`, held to
7
+ * the list's ends: `-Infinity` is the first item and `Infinity` the last. A
8
+ * move that cannot go anywhere returns `index` itself, so `next === index`
9
+ * says nothing moved and the caller can skip a repaint. An empty list has
10
+ * nowhere to move to and returns `index`.
11
+ */
12
+ export function moveSelection(index, delta, length) {
13
+ return length === 0 ? index : Math.max(0, Math.min(length - 1, index + delta));
14
+ }
@@ -70,83 +70,114 @@ export declare class Strip<T extends StyledRenderable = StyledRenderable> implem
70
70
  };
71
71
  }
72
72
  /**
73
- * The least ΔE_OK two neighbouring backgrounds must differ by for the powerline
74
- * arrow between them to be seen. Below it the arrow is drawn in a colour the
75
- * eye cannot tell from its own background, so the joiner draws the divider
73
+ * A colour a joiner's cell is drawn in. Each variant names where the colour
74
+ * comes from when the join is drawn:
75
+ * • an `edge` — a neighbour's ground (`bg`) or text (`fg`) at the column
76
+ * facing the join, read through `StyledRenderable.edgeStyle`;
77
+ * • a `role` — a style name, resolved against the render's theme, whose
78
+ * foreground colour is the colour;
79
+ * • a `mix` — two colours blended at `t` (0 is the first, 1 the second).
80
+ */
81
+ export type Paint = {
82
+ readonly edge: "left" | "right";
83
+ readonly part: "bg" | "fg";
84
+ } | {
85
+ readonly role: string;
86
+ } | {
87
+ readonly mix: readonly [Paint, Paint, number];
88
+ };
89
+ /**
90
+ * A `Paint`, or `"ground"`: the terminal's own background, whose colour nobody
91
+ * knows. That is why it is never mixed (`Paint` excludes it), and why a cell
92
+ * may put it on one side only. As a cell's `fg` it is drawn in reverse video —
93
+ * the glyph's ink shows the terminal's background through a cell filled with
94
+ * the `bg` colour — so an inverted shape is the same glyph, never a glyph of
95
+ * its own.
96
+ */
97
+ export type CellColor = Paint | "ground";
98
+ /**
99
+ * One styled run of a join. A `oneOf` glyph is a set any member of which may
100
+ * stand in the cell; every member has the same width, because consumers
101
+ * measure a join by rendering it.
102
+ */
103
+ export interface JoinCell {
104
+ readonly glyph: string | {
105
+ readonly oneOf: readonly string[];
106
+ };
107
+ readonly fg: CellColor;
108
+ readonly bg: CellColor;
109
+ }
110
+ /**
111
+ * The cells a joiner draws at each kind of join:
112
+ * • `seam` — between two neighbours;
113
+ * • `divider` — in the seam's place where the two grounds are drawn as one
114
+ * colour (see `SEAM_MIN_DELTA_E`), so the seam would vanish into them;
115
+ * the same width as `seam`;
116
+ * • `lead` — where a run is entered from nothing: the strip's start, or a
117
+ * neighbour with no ground for the seam to paint. It cannot name `left`;
118
+ * • `tail` — where a run leaves into nothing. It cannot name `right`.
119
+ */
120
+ export interface JoinShape {
121
+ readonly seam: readonly JoinCell[];
122
+ readonly divider: readonly JoinCell[];
123
+ readonly lead: readonly JoinCell[];
124
+ readonly tail: readonly JoinCell[];
125
+ }
126
+ /**
127
+ * The least ΔE_OK two neighbouring backgrounds must differ by for a seam drawn
128
+ * between them to be seen. Below it the seam is drawn in a colour the eye
129
+ * cannot tell from its own background, so the joiner draws the divider
76
130
  * instead. Twice the ~.02 threshold of a visible difference, because a seam is
77
131
  * one cell wide.
78
132
  */
79
133
  export declare const SEAM_MIN_DELTA_E = 0.04;
80
- export interface PowerlineJoinerOptions {
81
- /** Glyph for every join between two coloured items. */
82
- glyph: string;
83
- /**
84
- * Glyph drawn between neighbours whose backgrounds the eye cannot tell apart,
85
- * in the left item's text colour — the arrow itself would vanish into the
86
- * shared background.
87
- */
88
- divider: string;
89
- /**
90
- * Glyph for every join a coloured item is entered from nothing — the strip's
91
- * start, or a colourless left neighbour — painted in the right item's
92
- * background. `""` begins a coloured run flat.
93
- */
94
- lead: string;
95
- /**
96
- * Glyph for every join a coloured item leaves into nothing — the strip's end,
97
- * or a colourless right neighbour — painted in the left item's background.
98
- * `""` ends a coloured run flat.
99
- */
100
- tail: string;
101
- }
102
134
  /**
103
- * The powerline set: U+E0B0 (right-arrow) divided by U+E0B1 (thin right-arrow),
104
- * led by U+E0D7 (Nerd Fonts ple-left_hard_divider_inverse, the right-arrow's
105
- * inverse) and tailed by the arrow itself. The lead leaves empty a triangle
106
- * whose base is its cell's left edge and whose point touches the first item, so
107
- * the run opens notched — as if an arrow before it pointed in — rather than
108
- * with a shape pointing out. The notch is the terminal's own background showing
109
- * through, never a colour painted to imitate it, so it matches under a
110
- * translucent terminal background too. The lead is a Powerline Extra glyph: a
111
- * classic powerline font lacks it.
135
+ * The one joiner: what it draws at each join is the `JoinShape` it is given,
136
+ * so a new joiner is a value, never a class.
137
+ *
138
+ * Which part a join draws is read off its two neighbours. A neighbour is
139
+ * missing at the strip's ends, and counts as missing — an endpoint — when the
140
+ * shape paints with its ground anywhere and it has none to paint. With both present the
141
+ * join is the seam, or the divider where the two grounds are drawn as one
142
+ * colour at `options.colorSystem`; with only the right, the lead; with only
143
+ * the left, the tail; with neither, nothing.
144
+ *
145
+ * `ascii` is the shape an ASCII-only output draws instead, whole.
112
146
  */
113
- export declare const POWERLINE_JOINER_GLYPHS: Readonly<PowerlineJoinerOptions>;
114
- export declare class PowerlineJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
115
- private readonly _glyphs;
116
- constructor(options?: PowerlineJoinerOptions);
117
- join(left: T | null, right: T | null): Renderable;
118
- }
119
- export interface CapsuleJoinerOptions {
120
- /** Left-cap glyph (default: U+E0B6, powerline rounded left). */
121
- left?: string;
122
- /** Right-cap glyph (default: U+E0B4, powerline rounded right). */
123
- right?: string;
124
- /** Separator inserted between adjacent capsules in the middle position. */
125
- separator?: string;
126
- }
127
- export declare class CapsuleJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
128
- private readonly _caps;
129
- private readonly _separator;
130
- constructor(options?: CapsuleJoinerOptions);
131
- _emit(left: T | null, right: T | null, options: RenderOptions): Iterable<Segment>;
132
- join(left: T | null, right: T | null): Renderable;
133
- }
134
- export interface PlainJoinerOptions {
135
- separator?: string;
136
- style?: Style;
137
- }
138
- export declare class PlainJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
139
- private readonly _separator;
140
- private readonly _style;
141
- constructor(options?: PlainJoinerOptions);
142
- join(left: T | null, right: T | null): Renderable;
143
- }
144
- export interface GradientJoinerOptions {
145
- /** Number of cells between adjacent items (default: 4). */
146
- steps?: number;
147
- }
148
- export declare class GradientJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
149
- private readonly _steps;
150
- constructor(options?: GradientJoinerOptions);
147
+ export declare class CellJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
148
+ readonly shape: JoinShape;
149
+ readonly ascii: JoinShape;
150
+ private readonly _forms;
151
+ constructor(shape: JoinShape, ascii: JoinShape);
151
152
  join(left: T | null, right: T | null): Renderable;
152
153
  }
154
+ /**
155
+ * The powerline joiner: U+E0B0 (right-arrow) in the left ground over the
156
+ * right, divided by U+E0B1 (thin right-arrow) in the left item's text. The
157
+ * lead is the arrow with the left ground replaced by the terminal's — drawn
158
+ * in reverse video it leaves a notch whose base is the cell's left edge, inked
159
+ * in the terminal's background colour. That ink is opaque, so under a
160
+ * translucent terminal the notch does not let what is behind it through. The
161
+ * tail is the arrow over the terminal's ground.
162
+ * ASCII draws `>` everywhere and begins each run flat.
163
+ */
164
+ export declare const POWERLINE_JOINER: CellJoiner<StyledRenderable>;
165
+ /**
166
+ * Rounded pills: each run opened by U+E0B6 and closed by U+E0B4, both in its
167
+ * ground over the terminal's, a space between pills. ASCII draws `(` and `)`.
168
+ */
169
+ export declare const CAPSULE_JOINER: CellJoiner<StyledRenderable>;
170
+ /**
171
+ * ` | ` between every pair of items, in the theme's `strip.separator` colour;
172
+ * nothing at the ends. Its shape names no neighbour, so it joins items with no
173
+ * ground too.
174
+ */
175
+ export declare const PLAIN_JOINER: CellJoiner<StyledRenderable>;
176
+ /**
177
+ * A ramp of `steps` cells from the left ground to the right. Each cell is the
178
+ * half block U+258C, ink on its left half and ground on its right, so it
179
+ * carries two samples; ASCII draws a space, keeping the grounds alone. Nothing
180
+ * at the ends, or beside a neighbour with no ground — there is no colour to
181
+ * ramp toward.
182
+ */
183
+ export declare function gradientJoiner(steps?: number): CellJoiner;