@promptctl/rich-js 0.19.0 → 0.21.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 (58) hide show
  1. package/README.md +3 -2
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +10 -0
  4. package/dist/core/color.d.ts +1 -0
  5. package/dist/core/color.js +14 -3
  6. package/dist/core/noise.d.ts +31 -0
  7. package/dist/core/noise.js +94 -0
  8. package/dist/core/paint.js +13 -2
  9. package/dist/core/pretty.d.ts +23 -2
  10. package/dist/core/pretty.js +54 -18
  11. package/dist/core/protocol.d.ts +2 -0
  12. package/dist/core/protocol.js +3 -2
  13. package/dist/core/selection.d.ts +12 -0
  14. package/dist/core/selection.js +14 -0
  15. package/dist/core/strip.d.ts +159 -76
  16. package/dist/core/strip.js +367 -215
  17. package/dist/core/style.js +10 -7
  18. package/dist/host/app.d.ts +68 -2
  19. package/dist/host/app.js +125 -15
  20. package/dist/host/index.d.ts +3 -1
  21. package/dist/host/index.js +1 -0
  22. package/dist/host/input.d.ts +87 -0
  23. package/dist/host/input.js +517 -0
  24. package/dist/host/terminal-host.d.ts +12 -0
  25. package/dist/host/terminal-host.js +2 -0
  26. package/dist/index.d.ts +11 -6
  27. package/dist/index.js +7 -4
  28. package/dist/node/terminal-host.d.ts +7 -0
  29. package/dist/node/terminal-host.js +7 -0
  30. package/dist/renderables/effect.d.ts +89 -16
  31. package/dist/renderables/effect.js +115 -42
  32. package/dist/renderables/effects.d.ts +260 -0
  33. package/dist/renderables/effects.js +596 -0
  34. package/dist/renderables/embed.d.ts +11 -5
  35. package/dist/renderables/embed.js +13 -6
  36. package/dist/renderables/flexStrip.d.ts +1 -1
  37. package/dist/renderables/flexStrip.js +10 -9
  38. package/dist/renderables/list-view.d.ts +42 -0
  39. package/dist/renderables/list-view.js +78 -0
  40. package/dist/renderables/progress.d.ts +6 -0
  41. package/dist/renderables/progress.js +11 -4
  42. package/dist/renderables/progressBar.d.ts +22 -2
  43. package/dist/renderables/progressBar.js +52 -7
  44. package/dist/renderables/rule.d.ts +2 -2
  45. package/dist/renderables/rule.js +12 -10
  46. package/dist/renderables/table.js +5 -7
  47. package/dist/renderables/viewport.d.ts +30 -0
  48. package/dist/renderables/viewport.js +59 -3
  49. package/dist/widgets/dropdown.js +3 -3
  50. package/dist/widgets/event-router.d.ts +31 -40
  51. package/dist/widgets/event-router.js +52 -483
  52. package/dist/widgets/hit.js +2 -4
  53. package/dist/widgets/index.d.ts +2 -1
  54. package/dist/widgets/types.d.ts +8 -25
  55. package/dist/widgets/types.js +8 -0
  56. package/dist/widgets/widget-app.d.ts +22 -7
  57. package/dist/widgets/widget-app.js +22 -12
  58. package/package.json +26 -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
 
@@ -43,3 +43,8 @@ export interface Anchor {
43
43
  * every level of the nesting — each level names the same cell.
44
44
  */
45
45
  export declare function shiftAnchor(anchor: Anchor, cells: number): Anchor;
46
+ /**
47
+ * Whether `owner` drew the cell `anchor` names, itself or through an owner
48
+ * nested in its output.
49
+ */
50
+ export declare function drawnBy(anchor: Anchor | undefined, owner: object): boolean;
@@ -37,3 +37,13 @@ export function shiftAnchor(anchor, cells) {
37
37
  inner: anchor.inner && shiftAnchor(anchor.inner, cells),
38
38
  };
39
39
  }
40
+ /**
41
+ * Whether `owner` drew the cell `anchor` names, itself or through an owner
42
+ * nested in its output.
43
+ */
44
+ export function drawnBy(anchor, owner) {
45
+ for (let level = anchor; level; level = level.inner)
46
+ if (level.owner === owner)
47
+ return true;
48
+ return false;
49
+ }
@@ -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;
@@ -580,7 +581,17 @@ export class ColorSpec {
580
581
  throw new ColorParseError(`A blend's fraction is in [0, 1]; got ${t}`);
581
582
  const a = from.fixedValue;
582
583
  const b = to.fixedValue;
583
- return a !== undefined && b !== undefined ? ColorSpec.fromRgba(blendRgb(a, b, t)) : new ColorBlend(from, to, t);
584
+ if (a !== undefined && b !== undefined)
585
+ return ColorSpec.fromRgba(blendRgb(a, b, t));
586
+ // A blend that draws as one of its ends is that end — not a blend whose
587
+ // name no comparison will ever match back to it: at t 0 or 1, or between
588
+ // one slot of one palette spelled twice.
589
+ if (t === 0)
590
+ return from;
591
+ if (t === 1)
592
+ return to;
593
+ const oneSlot = from.type === to.type && (from.number !== undefined ? from.number === to.number : from.name === to.name);
594
+ return oneSlot ? from : new ColorBlend(from, to, t);
584
595
  }
585
596
  /**
586
597
  * The spec that draws `value` nearest to it at `depth` on a terminal known
@@ -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
+ }