@promptctl/rich-js 0.18.1 → 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 (169) hide show
  1. package/README.md +11 -8
  2. package/dist/core/box.d.ts +3 -1
  3. package/dist/core/box.js +6 -13
  4. package/dist/core/cells.d.ts +61 -28
  5. package/dist/core/cells.js +171 -124
  6. package/dist/core/clock.d.ts +48 -0
  7. package/dist/core/clock.js +42 -0
  8. package/dist/core/color.d.ts +99 -18
  9. package/dist/core/color.js +334 -146
  10. package/dist/core/console.d.ts +38 -7
  11. package/dist/core/console.js +159 -90
  12. package/dist/core/easing.d.ts +86 -0
  13. package/dist/core/easing.js +171 -0
  14. package/dist/core/env.d.ts +16 -0
  15. package/dist/core/env.js +16 -0
  16. package/dist/core/export-html.js +1 -4
  17. package/dist/core/export-lines.d.ts +22 -13
  18. package/dist/core/export-lines.js +37 -19
  19. package/dist/core/export-svg.d.ts +49 -0
  20. package/dist/core/export-svg.js +213 -0
  21. package/dist/core/fnv1a.d.ts +10 -0
  22. package/dist/core/fnv1a.js +17 -0
  23. package/dist/core/highlighter.d.ts +2 -2
  24. package/dist/core/highlighter.js +113 -47
  25. package/dist/core/json.js +1 -1
  26. package/dist/core/markup.d.ts +28 -7
  27. package/dist/core/markup.js +235 -312
  28. package/dist/core/measure.d.ts +3 -3
  29. package/dist/core/measure.js +12 -7
  30. package/dist/core/memo.d.ts +19 -0
  31. package/dist/core/memo.js +34 -0
  32. package/dist/core/noise.d.ts +31 -0
  33. package/dist/core/noise.js +94 -0
  34. package/dist/core/osc8.d.ts +6 -6
  35. package/dist/core/osc8.js +9 -21
  36. package/dist/core/paint.d.ts +107 -0
  37. package/dist/core/paint.js +161 -0
  38. package/dist/core/place.d.ts +1 -1
  39. package/dist/core/place.js +4 -6
  40. package/dist/core/pretty.d.ts +126 -2
  41. package/dist/core/pretty.js +571 -105
  42. package/dist/core/protocol.d.ts +43 -3
  43. package/dist/core/protocol.js +23 -2
  44. package/dist/core/render.d.ts +33 -30
  45. package/dist/core/render.js +43 -83
  46. package/dist/core/segment.d.ts +6 -5
  47. package/dist/core/segment.js +11 -15
  48. package/dist/core/selection.d.ts +12 -0
  49. package/dist/core/selection.js +14 -0
  50. package/dist/core/strip.d.ts +116 -74
  51. package/dist/core/strip.js +266 -217
  52. package/dist/core/style.d.ts +27 -7
  53. package/dist/core/style.js +106 -41
  54. package/dist/core/text.d.ts +43 -16
  55. package/dist/core/text.js +180 -59
  56. package/dist/core/wrap.d.ts +5 -0
  57. package/dist/core/wrap.js +16 -3
  58. package/dist/host/app.d.ts +95 -17
  59. package/dist/host/app.js +201 -61
  60. package/dist/host/index.d.ts +4 -1
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +84 -0
  63. package/dist/host/input.js +498 -0
  64. package/dist/host/terminal-host.d.ts +15 -2
  65. package/dist/host/terminal-host.js +2 -0
  66. package/dist/index.d.ts +26 -14
  67. package/dist/index.js +17 -7
  68. package/dist/node/prompt.d.ts +13 -0
  69. package/dist/node/prompt.js +30 -9
  70. package/dist/node/save.d.ts +4 -3
  71. package/dist/node/save.js +6 -3
  72. package/dist/node/terminal-host.d.ts +10 -2
  73. package/dist/node/terminal-host.js +7 -0
  74. package/dist/node/traceback.d.ts +2 -1
  75. package/dist/node/traceback.js +3 -39
  76. package/dist/renderables/align.js +2 -9
  77. package/dist/renderables/columns.d.ts +45 -29
  78. package/dist/renderables/columns.js +142 -103
  79. package/dist/renderables/constrain.js +6 -12
  80. package/dist/renderables/effect.d.ts +113 -0
  81. package/dist/renderables/effect.js +182 -0
  82. package/dist/renderables/effects.d.ts +252 -0
  83. package/dist/renderables/effects.js +565 -0
  84. package/dist/renderables/embed.d.ts +94 -31
  85. package/dist/renderables/embed.js +141 -45
  86. package/dist/renderables/group.d.ts +6 -3
  87. package/dist/renderables/group.js +5 -0
  88. package/dist/renderables/layout.d.ts +6 -23
  89. package/dist/renderables/layout.js +24 -113
  90. package/dist/renderables/list-view.d.ts +42 -0
  91. package/dist/renderables/list-view.js +78 -0
  92. package/dist/renderables/live.d.ts +40 -16
  93. package/dist/renderables/live.js +96 -83
  94. package/dist/renderables/markdown.d.ts +16 -2
  95. package/dist/renderables/markdown.js +521 -118
  96. package/dist/renderables/padding.d.ts +22 -1
  97. package/dist/renderables/padding.js +30 -24
  98. package/dist/renderables/panel.d.ts +31 -33
  99. package/dist/renderables/panel.js +137 -141
  100. package/dist/renderables/progress.d.ts +110 -16
  101. package/dist/renderables/progress.js +212 -81
  102. package/dist/renderables/progressBar.d.ts +24 -4
  103. package/dist/renderables/progressBar.js +78 -23
  104. package/dist/renderables/prompt.d.ts +37 -15
  105. package/dist/renderables/prompt.js +125 -85
  106. package/dist/renderables/ratio.d.ts +123 -0
  107. package/dist/renderables/ratio.js +213 -0
  108. package/dist/renderables/rule.d.ts +3 -2
  109. package/dist/renderables/rule.js +50 -52
  110. package/dist/renderables/spinner.d.ts +28 -2
  111. package/dist/renderables/spinner.js +52 -19
  112. package/dist/renderables/status.d.ts +13 -7
  113. package/dist/renderables/status.js +15 -29
  114. package/dist/renderables/syntax.d.ts +37 -7
  115. package/dist/renderables/syntax.js +134 -83
  116. package/dist/renderables/table.d.ts +58 -20
  117. package/dist/renderables/table.js +373 -197
  118. package/dist/renderables/traceback.d.ts +5 -4
  119. package/dist/renderables/traceback.js +239 -68
  120. package/dist/renderables/tree.d.ts +10 -2
  121. package/dist/renderables/tree.js +61 -34
  122. package/dist/renderables/viewport.d.ts +30 -0
  123. package/dist/renderables/viewport.js +61 -7
  124. package/dist/template-bindings/color-funcs.d.ts +31 -9
  125. package/dist/template-bindings/color-funcs.js +40 -14
  126. package/dist/template-bindings/helpers.d.ts +4 -2
  127. package/dist/template-bindings/helpers.js +5 -20
  128. package/dist/template-bindings/index.d.ts +29 -17
  129. package/dist/template-bindings/index.js +36 -20
  130. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  131. package/dist/template-bindings/palette-funcs.js +11 -7
  132. package/dist/template-bindings/style-funcs.js +4 -2
  133. package/dist/themes/buildPalette.d.ts +24 -3
  134. package/dist/themes/buildPalette.js +52 -8
  135. package/dist/themes/colorMath.d.ts +29 -17
  136. package/dist/themes/colorMath.js +74 -48
  137. package/dist/themes/colorRef.d.ts +2 -17
  138. package/dist/themes/colorRef.js +13 -31
  139. package/dist/themes/palette.d.ts +24 -0
  140. package/dist/themes/palette.js +46 -3
  141. package/dist/themes/ramp.d.ts +21 -34
  142. package/dist/themes/ramp.js +23 -42
  143. package/dist/themes/registry.js +39 -28
  144. package/dist/widgets/button.d.ts +2 -6
  145. package/dist/widgets/button.js +9 -40
  146. package/dist/widgets/checkbox.d.ts +2 -5
  147. package/dist/widgets/checkbox.js +9 -31
  148. package/dist/widgets/dropdown.d.ts +2 -5
  149. package/dist/widgets/dropdown.js +17 -51
  150. package/dist/widgets/event-router.d.ts +31 -40
  151. package/dist/widgets/event-router.js +52 -483
  152. package/dist/widgets/index.d.ts +2 -1
  153. package/dist/widgets/ink.d.ts +22 -0
  154. package/dist/widgets/ink.js +31 -0
  155. package/dist/widgets/slider.d.ts +2 -5
  156. package/dist/widgets/slider.js +11 -34
  157. package/dist/widgets/text-input.d.ts +19 -10
  158. package/dist/widgets/text-input.js +156 -147
  159. package/dist/widgets/themed-widget.d.ts +20 -0
  160. package/dist/widgets/themed-widget.js +89 -0
  161. package/dist/widgets/toggle.d.ts +2 -5
  162. package/dist/widgets/toggle.js +8 -31
  163. package/dist/widgets/types.d.ts +8 -25
  164. package/dist/widgets/types.js +8 -0
  165. package/dist/widgets/widget-app.d.ts +24 -7
  166. package/dist/widgets/widget-app.js +35 -18
  167. package/dist/widgets/widget-base.d.ts +2 -0
  168. package/dist/widgets/widget-base.js +9 -2
  169. package/package.json +31 -17
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import type { Segment } from "./segment.js";
6
6
  import { type Style, type StyleSyntaxError, type Theme } from "./style.js";
7
- import type { ColorDepth } from "./color.js";
7
+ import { ColorDepth } from "./color.js";
8
+ import type { Highlighter } from "./highlighter.js";
8
9
  /**
9
10
  * What becomes of a line still wider than its width once wrapping is done.
10
11
  * `"fold"` breaks it across lines, `"crop"` cuts it at the edge, `"ellipsis"`
@@ -41,8 +42,30 @@ export interface RenderOptions {
41
42
  justify?: "left" | "center" | "right" | "full";
42
43
  overflow?: OverflowMethod;
43
44
  noWrap?: boolean;
44
- highlight?: unknown;
45
- markup?: unknown;
45
+ /**
46
+ * Whether a string handed to a renderable — a cell, a label, a pane — is
47
+ * read as markup when it is drawn. Absent, it is, as a `Console`'s is unless
48
+ * told otherwise. A `Console` passes its own setting, and `print`'s.
49
+ */
50
+ markup?: boolean;
51
+ /**
52
+ * Whether a string handed to a renderable is highlighted when it is drawn,
53
+ * by `highlighter`. Absent, it is, as a `Console`'s is unless told
54
+ * otherwise. A `Console` passes its own setting, and `print`'s; a renderable
55
+ * that draws its strings plain, as Rich's `Panel`, `Table` and `Tree` do by
56
+ * default, sets it false for what it renders — Rich's
57
+ * `options.update(highlight=False)` — and a table column's `highlight` sets
58
+ * it either way. `activeHighlighter` is the one reading of the pair.
59
+ */
60
+ highlight?: boolean;
61
+ /**
62
+ * What highlights a string handed to a renderable, while `highlight` says
63
+ * to. A `Console` passes its highlighter whether or not its own `highlight`
64
+ * is on, as Rich's `render_str` reaches `console.highlighter`, so content
65
+ * that turns highlighting on finds the console's highlighter there. Absent,
66
+ * nothing highlights.
67
+ */
68
+ highlighter?: Highlighter;
46
69
  /**
47
70
  * The depth the output will be encoded at, `null` when it carries no colour.
48
71
  * A renderable choosing between two ways of drawing something — the strip's
@@ -73,6 +96,8 @@ export interface RenderOptions {
73
96
  */
74
97
  onDraw?: (owner: object) => void;
75
98
  }
99
+ /** Whether `text` is all ASCII — what an `asciiOnly` output can draw. */
100
+ export declare const isAscii: (text: string) => boolean;
76
101
  /**
77
102
  * `chosen` as this output can draw it: `ascii` in its place when the output is
78
103
  * ASCII-only and `chosen` spends a glyph outside ASCII, otherwise `chosen`
@@ -177,6 +202,21 @@ export type StyleErrorHandler = (error: StyleSyntaxError, style: string) => void
177
202
  * theme })` changed nothing.
178
203
  */
179
204
  export declare function getStyle(options: RenderOptions, style: string | Style): Style;
205
+ /**
206
+ * `style` as the depth the render is encoded at can draw it. A style that
207
+ * paints its own ground in fixed colours chose its ink and ground as a pair;
208
+ * at sixteen colours the writer rounds a fixed colour to an ANSI slot, which
209
+ * the terminal paints in its own theme's colour, and no pair of slots reads in
210
+ * every bundled theme. So where the depth would turn a fixed colour of the
211
+ * pair into a slot, the pair is dropped and the run is drawn on the
212
+ * terminal's own foreground and background, which its theme does make
213
+ * readable; its attributes still set it apart. A style whose colours are
214
+ * slots already (`white on blue`), or that paints no ground (`bold magenta`),
215
+ * was written for the terminal's own colours and is drawn as written. Rich
216
+ * draws a highlighted line number the same way below 256 colours
217
+ * (`Syntax._get_number_styles`).
218
+ */
219
+ export declare function groundedStyle(options: RenderOptions, style: Style): Style;
180
220
  /**
181
221
  * The options a renderable should work from: the caller's, with `maxWidth`
182
222
  * parsed into an actual count of cells.
@@ -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
@@ -60,6 +61,26 @@ export function fitHeight(lines, height) {
60
61
  export function getStyle(options, style) {
61
62
  return (options.theme ?? DEFAULT_THEME).resolve(style);
62
63
  }
64
+ /**
65
+ * `style` as the depth the render is encoded at can draw it. A style that
66
+ * paints its own ground in fixed colours chose its ink and ground as a pair;
67
+ * at sixteen colours the writer rounds a fixed colour to an ANSI slot, which
68
+ * the terminal paints in its own theme's colour, and no pair of slots reads in
69
+ * every bundled theme. So where the depth would turn a fixed colour of the
70
+ * pair into a slot, the pair is dropped and the run is drawn on the
71
+ * terminal's own foreground and background, which its theme does make
72
+ * readable; its attributes still set it apart. A style whose colours are
73
+ * slots already (`white on blue`), or that paints no ground (`bold magenta`),
74
+ * was written for the terminal's own colours and is drawn as written. Rich
75
+ * draws a highlighted line number the same way below 256 colours
76
+ * (`Syntax._get_number_styles`).
77
+ */
78
+ export function groundedStyle(options, style) {
79
+ const drawn = style.drawnColors(options.colorSystem ?? undefined);
80
+ const lost = (written, shown) => written?.fixedValue !== undefined && shown?.fixedValue === undefined;
81
+ const pairLost = style.bgcolor !== undefined && (lost(style.color, drawn.color) || lost(style.bgcolor, drawn.bgcolor));
82
+ return pairLost ? style.withoutColor : style;
83
+ }
63
84
  /**
64
85
  * The options a renderable should work from: the caller's, with `maxWidth`
65
86
  * parsed into an actual count of cells.
@@ -10,50 +10,57 @@
10
10
  * an explicit `ColorDepth` enum / non-`"auto"` spec, or supply `env` and
11
11
  * `isTTY` in the options so detection does not consult ambient process state.
12
12
  *
13
- * [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentsToString`
14
- * and is the single way segments become wire bytes. `Console._writeSegments`,
15
- * `Live.refresh`, `Screen`'s frame paint, `renderToString`, and
16
- * `segmentToString` all delegate here, so terminal output, live frames, widget
17
- * frames, string export, and single-segment encoding agree by construction.
13
+ * [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentToString`
14
+ * and is the single way a segment becomes wire bytes. `segmentsToString` is
15
+ * that encoding joined, and `Console._writeSegments`, `Painter` (every `Live`
16
+ * and `App` frame) and `renderToString` all go through it, so terminal output,
17
+ * live and app frames and string export agree by construction.
18
18
  *
19
19
  * [LAW:one-source-of-truth] What the encoding needs from where it writes
20
20
  * arrives as one `Destination` value. Each caller resolves its destination
21
21
  * once — spec, detection, and its own overrides — and hands it through whole,
22
22
  * so a depth is never paired with a link setting taken from somewhere else.
23
23
  *
24
- * [LAW:dataflow-not-control-flow] The same pipeline runs every render: collect
25
- * non-control non-empty pieces, partition by SGR-codes (SGR-runs), partition
26
- * each run by link (link-runs), emit one SGR open/close per run with link
27
- * open/close pairs sitting inside. Colour and hyperlinks are two facts about
28
- * the destination: `colorSystem === null` empties every piece's SGR-codes, and
29
- * `hyperlinks === false` empties every piece's link. A colour depth never
30
- * removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
31
- * follows OSC 8.
24
+ * Every segment is written alone, exactly as Python Rich 9d8f9a3 writes it
25
+ * (`Console._render_buffer` calls `Style.render` per segment): its SGR codes
26
+ * open, its text, a reset, and an OSC 8 pair around all three when it carries
27
+ * a link. Two adjacent segments with equal codes are two runs, not one. That
28
+ * costs a reset and a reopen the terminal draws identically, and it is what
29
+ * lets a fixture generated from the reference hold any sequence of segments,
30
+ * rather than only those in which no two neighbours happen to share a style
31
+ * (rich-render-g5g8). The price of that is that where a renderable cuts its
32
+ * segments is now in the bytes, so a renderable pinned against the reference
33
+ * has to cut where the reference cuts, as `Box`'s rules and `Table`'s blank
34
+ * cell lines do. A link split across segments still hovers as one link,
35
+ * because every pair it becomes carries the id `osc8Open` derives from the URL.
32
36
  *
33
- * [LAW:types-are-the-program] Adjacent same-style segments share an SGR wrap
34
- * because the SGR-codes string is the same group key for both — the
35
- * partitioning shape (data) encodes the byte structure; the emit walk is a
36
- * mechanical fold over it.
37
+ * Colour and hyperlinks are two facts about the destination:
38
+ * `colorSystem === null` empties every segment's SGR codes, and
39
+ * `hyperlinks === false` drops every segment's link. A colour depth never
40
+ * removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
41
+ * follows OSC 8. This is where the port departs from the reference, whose
42
+ * `Style.render` returns bare text for a null colour system.
37
43
  */
38
44
  import { ColorDepth } from "./color.js";
39
- import type { Destination } from "./color.js";
45
+ import type { ColorSystemName, Destination } from "./color.js";
46
+ import type { Env } from "./env.js";
40
47
  import type { Segment } from "./segment.js";
41
48
  import type { Renderable, RenderOptions } from "./protocol.js";
42
- export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "onStyleError"> {
49
+ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" | "theme" | "onStyleError" | "highlight" | "highlighter"> {
43
50
  /** Cell width to render into. Default 80. */
44
51
  width?: number;
45
52
  /**
46
- * Color encoding to emit. Accepts a string spec (`"auto"`, `"truecolor"`,
47
- * `"256"`, `"ansi"`, `"none"`), a `ColorDepth` enum value, or `null` to
53
+ * Color encoding to emit. Accepts a `ColorSystemName` (`"auto"`,
54
+ * `"truecolor"`, `"256"`, `"ansi"`, `"none"`), a `ColorDepth` enum value, or `null` to
48
55
  * strip SGR colors/attributes. Hyperlinks are `hyperlinks`' concern. Default
49
56
  * truecolor.
50
57
  */
51
- colorSystem?: string | ColorDepth | null;
58
+ colorSystem?: ColorSystemName | ColorDepth | null;
52
59
  /**
53
60
  * Environment to consult when `colorSystem` is `"auto"`. Defaults to
54
61
  * `process.env`. Pass an explicit value to keep rendering deterministic.
55
62
  */
56
- env?: NodeJS.ProcessEnv;
63
+ env?: Env;
57
64
  /**
58
65
  * Whether output is going to a TTY when `colorSystem` is `"auto"`. Defaults
59
66
  * to `process.stdout?.isTTY`. Pass an explicit value to keep rendering
@@ -71,14 +78,10 @@ export interface RenderToStringOptions extends Pick<RenderOptions, "asciiOnly" |
71
78
  hyperlinks?: boolean;
72
79
  }
73
80
  /**
74
- * Encodes a single segment as ANSI bytes for `destination`. Equivalent to
75
- * `segmentsToString([segment], destination)` — same SGR / OSC 8 layout.
81
+ * Encodes one segment as ANSI bytes for `destination`: the reference's
82
+ * `Style.render` for that segment, and nothing for a control segment.
76
83
  */
77
84
  export declare function segmentToString(segment: Segment, destination: Destination): string;
78
- /**
79
- * Encodes a sequence of segments as ANSI bytes for `destination`, coalescing
80
- * adjacent same-SGR segments under a single SGR open/close pair, with OSC 8
81
- * link pairs nested inside per same-link sub-run.
82
- */
85
+ /** Encodes a sequence of segments as ANSI bytes for `destination`, one segment at a time. */
83
86
  export declare function segmentsToString(segments: Iterable<Segment>, destination: Destination): string;
84
87
  export declare function renderToString(renderable: Renderable, options?: RenderToStringOptions): string;
@@ -10,106 +10,63 @@
10
10
  * an explicit `ColorDepth` enum / non-`"auto"` spec, or supply `env` and
11
11
  * `isTTY` in the options so detection does not consult ambient process state.
12
12
  *
13
- * [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentsToString`
14
- * and is the single way segments become wire bytes. `Console._writeSegments`,
15
- * `Live.refresh`, `Screen`'s frame paint, `renderToString`, and
16
- * `segmentToString` all delegate here, so terminal output, live frames, widget
17
- * frames, string export, and single-segment encoding agree by construction.
13
+ * [LAW:single-enforcer] The Segment-to-ANSI conversion lives in `segmentToString`
14
+ * and is the single way a segment becomes wire bytes. `segmentsToString` is
15
+ * that encoding joined, and `Console._writeSegments`, `Painter` (every `Live`
16
+ * and `App` frame) and `renderToString` all go through it, so terminal output,
17
+ * live and app frames and string export agree by construction.
18
18
  *
19
19
  * [LAW:one-source-of-truth] What the encoding needs from where it writes
20
20
  * arrives as one `Destination` value. Each caller resolves its destination
21
21
  * once — spec, detection, and its own overrides — and hands it through whole,
22
22
  * so a depth is never paired with a link setting taken from somewhere else.
23
23
  *
24
- * [LAW:dataflow-not-control-flow] The same pipeline runs every render: collect
25
- * non-control non-empty pieces, partition by SGR-codes (SGR-runs), partition
26
- * each run by link (link-runs), emit one SGR open/close per run with link
27
- * open/close pairs sitting inside. Colour and hyperlinks are two facts about
28
- * the destination: `colorSystem === null` empties every piece's SGR-codes, and
29
- * `hyperlinks === false` empties every piece's link. A colour depth never
30
- * removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
31
- * follows OSC 8.
24
+ * Every segment is written alone, exactly as Python Rich 9d8f9a3 writes it
25
+ * (`Console._render_buffer` calls `Style.render` per segment): its SGR codes
26
+ * open, its text, a reset, and an OSC 8 pair around all three when it carries
27
+ * a link. Two adjacent segments with equal codes are two runs, not one. That
28
+ * costs a reset and a reopen the terminal draws identically, and it is what
29
+ * lets a fixture generated from the reference hold any sequence of segments,
30
+ * rather than only those in which no two neighbours happen to share a style
31
+ * (rich-render-g5g8). The price of that is that where a renderable cuts its
32
+ * segments is now in the bytes, so a renderable pinned against the reference
33
+ * has to cut where the reference cuts, as `Box`'s rules and `Table`'s blank
34
+ * cell lines do. A link split across segments still hovers as one link,
35
+ * because every pair it becomes carries the id `osc8Open` derives from the URL.
32
36
  *
33
- * [LAW:types-are-the-program] Adjacent same-style segments share an SGR wrap
34
- * because the SGR-codes string is the same group key for both — the
35
- * partitioning shape (data) encodes the byte structure; the emit walk is a
36
- * mechanical fold over it.
37
+ * Colour and hyperlinks are two facts about the destination:
38
+ * `colorSystem === null` empties every segment's SGR codes, and
39
+ * `hyperlinks === false` drops every segment's link. A colour depth never
40
+ * removes a link — a hyperlink is not a colour, and a NO_COLOR terminal still
41
+ * follows OSC 8. This is where the port departs from the reference, whose
42
+ * `Style.render` returns bare text for a null colour system.
37
43
  */
38
44
  import { ColorDepth, resolveDestination } from "./color.js";
39
45
  import { OSC8_CLOSE, osc8Open } from "./osc8.js";
40
46
  const DEFAULT_WIDTH = 80;
41
- function segmentToPiece(segment, destination) {
42
- if (segment.isControl)
43
- return undefined;
44
- if (segment.text.length === 0)
45
- return undefined;
46
- const style = segment.style;
47
- if (!style || style.isNull) {
48
- return { text: segment.text, sgrCodes: "", link: undefined };
49
- }
50
- // [LAW:one-type-per-behavior] Colour depth governs SGR only; hyperlinks are
51
- // their own fact. Coupling them let a colour setting delete every control.
52
- return {
53
- text: segment.text,
54
- sgrCodes: destination.colorSystem === null ? "" : style.toSgrCodes(destination.colorSystem),
55
- link: destination.hyperlinks ? style.link : undefined,
56
- };
57
- }
58
47
  /**
59
- * Encodes a single segment as ANSI bytes for `destination`. Equivalent to
60
- * `segmentsToString([segment], destination)` — same SGR / OSC 8 layout.
48
+ * Encodes one segment as ANSI bytes for `destination`: the reference's
49
+ * `Style.render` for that segment, and nothing for a control segment.
61
50
  */
62
51
  export function segmentToString(segment, destination) {
63
- return segmentsToString([segment], destination);
52
+ if (segment.isControl)
53
+ return "";
54
+ const { text, style } = segment;
55
+ // As the reference's `if not text` and `if style:` — no pair around nothing.
56
+ if (text.length === 0 || style === undefined)
57
+ return text;
58
+ // [LAW:one-type-per-behavior] Colour depth governs SGR only; hyperlinks are
59
+ // their own fact. Coupling them let a colour setting delete every control.
60
+ const codes = destination.colorSystem === null ? "" : style.toSgrCodes(destination.colorSystem);
61
+ const styled = codes.length > 0 ? `\x1b[${codes}m${text}\x1b[0m` : text;
62
+ const link = destination.hyperlinks ? style.link : undefined;
63
+ return link === undefined ? styled : `${osc8Open(link)}${styled}${OSC8_CLOSE}`;
64
64
  }
65
- /**
66
- * Encodes a sequence of segments as ANSI bytes for `destination`, coalescing
67
- * adjacent same-SGR segments under a single SGR open/close pair, with OSC 8
68
- * link pairs nested inside per same-link sub-run.
69
- */
65
+ /** Encodes a sequence of segments as ANSI bytes for `destination`, one segment at a time. */
70
66
  export function segmentsToString(segments, destination) {
71
- const pieces = [];
72
- for (const s of segments) {
73
- const p = segmentToPiece(s, destination);
74
- if (p)
75
- pieces.push(p);
76
- }
77
- if (pieces.length === 0)
78
- return "";
79
- // [LAW:dataflow-not-control-flow] One linear chunk accumulator for the
80
- // entire output; SGR / OSC 8 boundaries and piece texts all push into it
81
- // in order. No per-link-run intermediate string, no quadratic `+=` chains.
82
67
  const parts = [];
83
- let i = 0;
84
- while (i < pieces.length) {
85
- const sgr = pieces[i].sgrCodes;
86
- let j = i + 1;
87
- while (j < pieces.length && pieces[j].sgrCodes === sgr)
88
- j++;
89
- if (sgr.length > 0)
90
- parts.push(`\x1b[${sgr}m`);
91
- let k = i;
92
- while (k < j) {
93
- const link = pieces[k].link;
94
- let l = k + 1;
95
- while (l < j && pieces[l].link === link)
96
- l++;
97
- if (link) {
98
- parts.push(osc8Open(link));
99
- for (let m = k; m < l; m++)
100
- parts.push(pieces[m].text);
101
- parts.push(OSC8_CLOSE);
102
- }
103
- else {
104
- for (let m = k; m < l; m++)
105
- parts.push(pieces[m].text);
106
- }
107
- k = l;
108
- }
109
- if (sgr.length > 0)
110
- parts.push("\x1b[0m");
111
- i = j;
112
- }
68
+ for (const segment of segments)
69
+ parts.push(segmentToString(segment, destination));
113
70
  return parts.join("");
114
71
  }
115
72
  export function renderToString(renderable, options) {
@@ -136,7 +93,10 @@ export function renderToString(renderable, options) {
136
93
  maxWidth: width,
137
94
  isTerminal: false,
138
95
  asciiOnly: options?.asciiOnly ?? false,
96
+ theme: options?.theme,
139
97
  onStyleError: options?.onStyleError,
98
+ highlight: options?.highlight,
99
+ highlighter: options?.highlighter,
140
100
  // [LAW:one-source-of-truth] The depth the segments below are encoded at,
141
101
  // so a renderable measures what this very call will draw.
142
102
  colorSystem: destination.colorSystem,
@@ -33,12 +33,13 @@ export declare class Segment {
33
33
  get hasText(): boolean;
34
34
  get isControl(): boolean;
35
35
  /**
36
- * Splits at a cell position. Returns [left, right].
36
+ * Splits at a cell position. Returns [left, right]: the first `position`
37
+ * cells and the rest, so the two halves are exactly as wide as this one —
38
+ * `cutCells` blanks a wide glyph the cut goes through rather than keep it
39
+ * whole on a side it would widen by a cell.
37
40
  *
38
- * [LAW:single-enforcer] The right half starts as many cells further into
39
- * whatever drew it as the left half took from this one, and its anchor says
40
- * so; see `./anchor.ts`. That is not `leftText`'s width: a cut through a
41
- * wide glyph pads the left half and keeps the whole glyph on the right.
41
+ * [LAW:single-enforcer] The right half starts `position` cells further into
42
+ * whatever drew it, and its anchor says so; see `./anchor.ts`.
42
43
  */
43
44
  splitCells(position: CellCol): [Segment, Segment];
44
45
  private static _line;
@@ -2,7 +2,7 @@
2
2
  * Segment — the atomic rendering unit. Every piece of styled terminal output
3
3
  * is represented as a Segment: (text, style?, control?).
4
4
  */
5
- import { cellLen, splitText, asCellCol } from "./cells.js";
5
+ import { cellLen, cutCells, splitText, asCellCol } from "./cells.js";
6
6
  import { Style } from "./style.js";
7
7
  import { shiftAnchor } from "./anchor.js";
8
8
  // --- ControlType ---
@@ -45,12 +45,13 @@ export class Segment {
45
45
  return this.control !== undefined;
46
46
  }
47
47
  /**
48
- * Splits at a cell position. Returns [left, right].
48
+ * Splits at a cell position. Returns [left, right]: the first `position`
49
+ * cells and the rest, so the two halves are exactly as wide as this one —
50
+ * `cutCells` blanks a wide glyph the cut goes through rather than keep it
51
+ * whole on a side it would widen by a cell.
49
52
  *
50
- * [LAW:single-enforcer] The right half starts as many cells further into
51
- * whatever drew it as the left half took from this one, and its anchor says
52
- * so; see `./anchor.ts`. That is not `leftText`'s width: a cut through a
53
- * wide glyph pads the left half and keeps the whole glyph on the right.
53
+ * [LAW:single-enforcer] The right half starts `position` cells further into
54
+ * whatever drew it, and its anchor says so; see `./anchor.ts`.
54
55
  */
55
56
  splitCells(position) {
56
57
  const len = this.cellLength;
@@ -58,10 +59,10 @@ export class Segment {
58
59
  return [this, new Segment("")];
59
60
  if (position <= 0)
60
61
  return [new Segment(""), this];
61
- const [leftText, rightText] = splitText(this.text, position);
62
+ const [leftText, rightText] = cutCells(this.text, position);
62
63
  return [
63
64
  new Segment(leftText, this.style),
64
- new Segment(rightText, this.style?.shiftedBy(len - cellLen(rightText))),
65
+ new Segment(rightText, this.style?.shiftedBy(position)),
65
66
  ];
66
67
  }
67
68
  // --- Static factories ---
@@ -394,13 +395,8 @@ export class Segment {
394
395
  for (const cut of cuts) {
395
396
  const section = [];
396
397
  while (currentSegment && cellOffset + currentSegment.cellLength <= cut) {
397
- if (currentSegment.isControl) {
398
- section.push(currentSegment);
399
- }
400
- else {
401
- section.push(currentSegment);
402
- cellOffset += currentSegment.cellLength;
403
- }
398
+ section.push(currentSegment);
399
+ cellOffset += currentSegment.cellLength;
404
400
  segmentIndex++;
405
401
  currentSegment = segments[segmentIndex];
406
402
  }
@@ -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
+ }