@promptctl/rich-js 0.18.1 → 0.19.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 (150) hide show
  1. package/README.md +9 -7
  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 +98 -18
  9. package/dist/core/color.js +331 -144
  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/osc8.d.ts +6 -6
  33. package/dist/core/osc8.js +9 -21
  34. package/dist/core/paint.d.ts +107 -0
  35. package/dist/core/paint.js +150 -0
  36. package/dist/core/place.d.ts +1 -1
  37. package/dist/core/place.js +4 -6
  38. package/dist/core/pretty.d.ts +105 -2
  39. package/dist/core/pretty.js +527 -97
  40. package/dist/core/protocol.d.ts +41 -3
  41. package/dist/core/protocol.js +20 -0
  42. package/dist/core/render.d.ts +33 -30
  43. package/dist/core/render.js +43 -83
  44. package/dist/core/segment.d.ts +6 -5
  45. package/dist/core/segment.js +11 -15
  46. package/dist/core/strip.d.ts +13 -2
  47. package/dist/core/strip.js +44 -23
  48. package/dist/core/style.d.ts +27 -7
  49. package/dist/core/style.js +97 -34
  50. package/dist/core/text.d.ts +43 -16
  51. package/dist/core/text.js +180 -59
  52. package/dist/core/wrap.d.ts +5 -0
  53. package/dist/core/wrap.js +16 -3
  54. package/dist/host/app.d.ts +31 -19
  55. package/dist/host/app.js +90 -60
  56. package/dist/host/index.d.ts +2 -1
  57. package/dist/host/terminal-host.d.ts +3 -2
  58. package/dist/index.d.ts +17 -10
  59. package/dist/index.js +11 -4
  60. package/dist/node/prompt.d.ts +13 -0
  61. package/dist/node/prompt.js +30 -9
  62. package/dist/node/save.d.ts +4 -3
  63. package/dist/node/save.js +6 -3
  64. package/dist/node/terminal-host.d.ts +3 -2
  65. package/dist/node/traceback.d.ts +2 -1
  66. package/dist/node/traceback.js +3 -39
  67. package/dist/renderables/align.js +2 -9
  68. package/dist/renderables/columns.d.ts +45 -29
  69. package/dist/renderables/columns.js +142 -103
  70. package/dist/renderables/constrain.js +6 -12
  71. package/dist/renderables/effect.d.ts +98 -0
  72. package/dist/renderables/effect.js +160 -0
  73. package/dist/renderables/embed.d.ts +87 -30
  74. package/dist/renderables/embed.js +134 -45
  75. package/dist/renderables/group.d.ts +6 -3
  76. package/dist/renderables/group.js +5 -0
  77. package/dist/renderables/layout.d.ts +6 -23
  78. package/dist/renderables/layout.js +24 -113
  79. package/dist/renderables/live.d.ts +40 -16
  80. package/dist/renderables/live.js +96 -83
  81. package/dist/renderables/markdown.d.ts +16 -2
  82. package/dist/renderables/markdown.js +521 -118
  83. package/dist/renderables/padding.d.ts +22 -1
  84. package/dist/renderables/padding.js +30 -24
  85. package/dist/renderables/panel.d.ts +31 -33
  86. package/dist/renderables/panel.js +137 -141
  87. package/dist/renderables/progress.d.ts +104 -16
  88. package/dist/renderables/progress.js +205 -81
  89. package/dist/renderables/progressBar.d.ts +2 -2
  90. package/dist/renderables/progressBar.js +30 -19
  91. package/dist/renderables/prompt.d.ts +37 -15
  92. package/dist/renderables/prompt.js +125 -85
  93. package/dist/renderables/ratio.d.ts +123 -0
  94. package/dist/renderables/ratio.js +213 -0
  95. package/dist/renderables/rule.d.ts +1 -0
  96. package/dist/renderables/rule.js +44 -48
  97. package/dist/renderables/spinner.d.ts +28 -2
  98. package/dist/renderables/spinner.js +52 -19
  99. package/dist/renderables/status.d.ts +13 -7
  100. package/dist/renderables/status.js +15 -29
  101. package/dist/renderables/syntax.d.ts +37 -7
  102. package/dist/renderables/syntax.js +134 -83
  103. package/dist/renderables/table.d.ts +58 -20
  104. package/dist/renderables/table.js +370 -192
  105. package/dist/renderables/traceback.d.ts +5 -4
  106. package/dist/renderables/traceback.js +239 -68
  107. package/dist/renderables/tree.d.ts +10 -2
  108. package/dist/renderables/tree.js +61 -34
  109. package/dist/renderables/viewport.js +2 -4
  110. package/dist/template-bindings/color-funcs.d.ts +31 -9
  111. package/dist/template-bindings/color-funcs.js +40 -14
  112. package/dist/template-bindings/helpers.d.ts +4 -2
  113. package/dist/template-bindings/helpers.js +5 -20
  114. package/dist/template-bindings/index.d.ts +29 -17
  115. package/dist/template-bindings/index.js +36 -20
  116. package/dist/template-bindings/palette-funcs.d.ts +3 -1
  117. package/dist/template-bindings/palette-funcs.js +11 -7
  118. package/dist/template-bindings/style-funcs.js +4 -2
  119. package/dist/themes/buildPalette.d.ts +24 -3
  120. package/dist/themes/buildPalette.js +52 -8
  121. package/dist/themes/colorMath.d.ts +29 -17
  122. package/dist/themes/colorMath.js +74 -48
  123. package/dist/themes/colorRef.d.ts +2 -17
  124. package/dist/themes/colorRef.js +13 -31
  125. package/dist/themes/palette.d.ts +24 -0
  126. package/dist/themes/palette.js +46 -3
  127. package/dist/themes/ramp.d.ts +21 -34
  128. package/dist/themes/ramp.js +23 -42
  129. package/dist/themes/registry.js +39 -28
  130. package/dist/widgets/button.d.ts +2 -6
  131. package/dist/widgets/button.js +9 -40
  132. package/dist/widgets/checkbox.d.ts +2 -5
  133. package/dist/widgets/checkbox.js +9 -31
  134. package/dist/widgets/dropdown.d.ts +2 -5
  135. package/dist/widgets/dropdown.js +14 -48
  136. package/dist/widgets/ink.d.ts +22 -0
  137. package/dist/widgets/ink.js +31 -0
  138. package/dist/widgets/slider.d.ts +2 -5
  139. package/dist/widgets/slider.js +11 -34
  140. package/dist/widgets/text-input.d.ts +19 -10
  141. package/dist/widgets/text-input.js +156 -147
  142. package/dist/widgets/themed-widget.d.ts +20 -0
  143. package/dist/widgets/themed-widget.js +89 -0
  144. package/dist/widgets/toggle.d.ts +2 -5
  145. package/dist/widgets/toggle.js +8 -31
  146. package/dist/widgets/widget-app.d.ts +2 -0
  147. package/dist/widgets/widget-app.js +13 -6
  148. package/dist/widgets/widget-base.d.ts +2 -0
  149. package/dist/widgets/widget-base.js +9 -2
  150. package/package.json +11 -1
@@ -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
@@ -177,6 +200,21 @@ export type StyleErrorHandler = (error: StyleSyntaxError, style: string) => void
177
200
  * theme })` changed nothing.
178
201
  */
179
202
  export declare function getStyle(options: RenderOptions, style: string | Style): Style;
203
+ /**
204
+ * `style` as the depth the render is encoded at can draw it. A style that
205
+ * paints its own ground in fixed colours chose its ink and ground as a pair;
206
+ * at sixteen colours the writer rounds a fixed colour to an ANSI slot, which
207
+ * the terminal paints in its own theme's colour, and no pair of slots reads in
208
+ * every bundled theme. So where the depth would turn a fixed colour of the
209
+ * pair into a slot, the pair is dropped and the run is drawn on the
210
+ * terminal's own foreground and background, which its theme does make
211
+ * readable; its attributes still set it apart. A style whose colours are
212
+ * slots already (`white on blue`), or that paints no ground (`bold magenta`),
213
+ * was written for the terminal's own colours and is drawn as written. Rich
214
+ * draws a highlighted line number the same way below 256 colours
215
+ * (`Syntax._get_number_styles`).
216
+ */
217
+ export declare function groundedStyle(options: RenderOptions, style: Style): Style;
180
218
  /**
181
219
  * The options a renderable should work from: the caller's, with `maxWidth`
182
220
  * parsed into an actual count of cells.
@@ -60,6 +60,26 @@ export function fitHeight(lines, height) {
60
60
  export function getStyle(options, style) {
61
61
  return (options.theme ?? DEFAULT_THEME).resolve(style);
62
62
  }
63
+ /**
64
+ * `style` as the depth the render is encoded at can draw it. A style that
65
+ * paints its own ground in fixed colours chose its ink and ground as a pair;
66
+ * at sixteen colours the writer rounds a fixed colour to an ANSI slot, which
67
+ * the terminal paints in its own theme's colour, and no pair of slots reads in
68
+ * every bundled theme. So where the depth would turn a fixed colour of the
69
+ * pair into a slot, the pair is dropped and the run is drawn on the
70
+ * terminal's own foreground and background, which its theme does make
71
+ * readable; its attributes still set it apart. A style whose colours are
72
+ * slots already (`white on blue`), or that paints no ground (`bold magenta`),
73
+ * was written for the terminal's own colours and is drawn as written. Rich
74
+ * draws a highlighted line number the same way below 256 colours
75
+ * (`Syntax._get_number_styles`).
76
+ */
77
+ export function groundedStyle(options, style) {
78
+ const drawn = style.drawnColors(options.colorSystem ?? undefined);
79
+ const lost = (written, shown) => written?.fixedValue !== undefined && shown?.fixedValue === undefined;
80
+ const pairLost = style.bgcolor !== undefined && (lost(style.color, drawn.color) || lost(style.bgcolor, drawn.bgcolor));
81
+ return pairLost ? style.withoutColor : style;
82
+ }
63
83
  /**
64
84
  * The options a renderable should work from: the caller's, with `maxWidth`
65
85
  * 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
  }
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { Segment } from "./segment.js";
29
29
  import { Style } from "./style.js";
30
- import { type Renderable, type RenderOptions } from "./protocol.js";
30
+ import { type Measurable, type Renderable, type RenderOptions } from "./protocol.js";
31
31
  /**
32
32
  * Items in a `Strip` expose their per-edge style so joiners can paint the
33
33
  * transition glyph at each boundary.
@@ -52,11 +52,22 @@ export interface Joiner<T extends StyledRenderable = StyledRenderable> {
52
52
  */
53
53
  join(left: T | null, right: T | null): Renderable;
54
54
  }
55
- export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable {
55
+ export declare class Strip<T extends StyledRenderable = StyledRenderable> implements Renderable, Measurable {
56
56
  readonly items: readonly T[];
57
57
  readonly joiner: Joiner<T>;
58
58
  constructor(items: readonly T[], joiner: Joiner<T>);
59
59
  render(options: RenderOptions): Iterable<Segment>;
60
+ /**
61
+ * [LAW:one-source-of-truth] The width of the one row it draws, and that is
62
+ * both bounds: the row never wraps and no item is offered less than the whole
63
+ * width, so there is no narrower strip to report as a minimum. Read off the
64
+ * drawing rather than summed from the items' measurements, which describe
65
+ * how an item could be drawn, not how the walk draws it.
66
+ */
67
+ measure(options: RenderOptions): {
68
+ minimum: number;
69
+ maximum: number;
70
+ };
60
71
  }
61
72
  /**
62
73
  * The least ΔE_OK two neighbouring backgrounds must differ by for the powerline
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { Segment } from "./segment.js";
29
29
  import { Style } from "./style.js";
30
- import { ColorDepth, ColorSpec, blendRgb } from "./color.js";
30
+ import { ColorDepth, ColorSpec, SURFACE_BLACK } from "./color.js";
31
31
  import { Oklch } from "./oklch.js";
32
32
  import { drawable } from "./protocol.js";
33
33
  // --- Strip ---
@@ -46,12 +46,17 @@ export class Strip {
46
46
  // start-cap, item, mid-join, item, ..., item, end-cap. Variability lives
47
47
  // in `items` and in what the joiner emits at each position — never in
48
48
  // whether a join runs.
49
- yield* this.joiner.join(null, items[0]).render(options);
49
+ //
50
+ // The row sits its items side by side, so none is handed a justify: a
51
+ // `left` fills the canvas, and the first item would pad over the rest of
52
+ // the row. Where the row sits in the width is its container's to say.
53
+ const row = { ...options, justify: undefined };
54
+ yield* this.joiner.join(null, items[0]).render(row);
50
55
  for (let i = 0; i < items.length; i++) {
51
56
  const item = items[i];
52
- yield* item.render(options);
57
+ yield* item.render(row);
53
58
  const next = i + 1 < items.length ? items[i + 1] : null;
54
- yield* this.joiner.join(item, next).render(options);
59
+ yield* this.joiner.join(item, next).render(row);
55
60
  }
56
61
  // [LAW:one-source-of-truth] `Strip`'s sibling `FlexStrip` ends every line
57
62
  // it emits, including the last, and the other block-level renderables
@@ -61,11 +66,23 @@ export class Strip {
61
66
  // under composition: printed alone, Console's own "close whatever the
62
67
  // renderable left open" step (console.ts) papered over it, and a `Group`
63
68
  // of a Strip and a RichText ran the RichText onto the Strip's line
64
- // instead of below it. (Not every renderable follows this — a `Spinner`
65
- // or `ProgressBar` is a line *fragment*, not a block; see
66
- // rich-flexstrip-5kf.4cq for the open question of formalizing that split.)
69
+ // instead of below it. `LINE_ENDS` in test/seam/line-ends.ts is the list
70
+ // of which renderables end their own line and which are fragments.
67
71
  yield Segment.line();
68
72
  }
73
+ /**
74
+ * [LAW:one-source-of-truth] The width of the one row it draws, and that is
75
+ * both bounds: the row never wraps and no item is offered less than the whole
76
+ * width, so there is no narrower strip to report as a minimum. Read off the
77
+ * drawing rather than summed from the items' measurements, which describe
78
+ * how an item could be drawn, not how the walk draws it.
79
+ */
80
+ measure(options) {
81
+ let width = 0;
82
+ for (const segment of this.render(options))
83
+ width += segment.cellLength;
84
+ return { minimum: width, maximum: width };
85
+ }
69
86
  }
70
87
  // --- Helpers ---
71
88
  const EMPTY = {
@@ -91,11 +108,11 @@ function bgAsFg(edge) {
91
108
  function paintableBg(bg) {
92
109
  return bg !== undefined && !bg.isDefault ? bg : undefined;
93
110
  }
94
- // A cap or arrow is its cell continuing: that cell's ground as the writer draws
95
- // it, so a translucent ground is not composited a second time over whatever it
96
- // enters.
111
+ // A cap, arrow or gradient is its cell continuing: that cell's ground as the
112
+ // writer draws it (`Style.drawnColors`, onto the terminal's black), so a
113
+ // translucent ground is not composited a second time over whatever it enters.
97
114
  function drawnGround(bg) {
98
- return new Style({ bgcolor: bg }).drawnColors().bgcolor;
115
+ return bg.flattenAlpha(SURFACE_BLACK);
99
116
  }
100
117
  // A cap is its glyph in its cell's ground, and "" is a flat end: no glyph, so
101
118
  // no segment — the same output as a join with nothing to paint.
@@ -117,10 +134,12 @@ export const SEAM_MIN_DELTA_E = 0.04;
117
134
  // encodes: the arrow's colour flattened onto its ground and the ground onto
118
135
  // the terminal's black, both downgraded to the depth the render encodes at,
119
136
  // so two grounds 256 colours round to one cube entry are the one entry they
120
- // render as. A colour whose RGB is the terminal theme's own (ANSI 0–15) has no
121
- // value here, so two of them are the same only when they are the same palette
122
- // slot — its `number`, never the name or the depth that spells it ("red",
123
- // "color(1)", an EIGHT_BIT spec on 0–15 are one slot).
137
+ // render as. A colour whose RGB is the terminal theme's own (ANSI 0–15, a
138
+ // blend reaching one) has no value here, so two of them are the same only
139
+ // when they are one expression: the same palette slot — its `number`, never
140
+ // the name or the depth that spells it ("red", "color(1)", an EIGHT_BIT spec
141
+ // on 0–15 are one slot) — or the same blend, whose name spells each end by its
142
+ // slot for the same reason.
124
143
  function vanishes(arrow, colorSystem) {
125
144
  // No colour emitted draws nothing to tell apart; measure what was handed.
126
145
  const { color, bgcolor } = arrow.drawnColors(colorSystem ?? ColorDepth.TRUECOLOR);
@@ -130,7 +149,7 @@ function vanishes(arrow, colorSystem) {
130
149
  const bv = bgcolor.fixedValue;
131
150
  return av !== undefined && bv !== undefined
132
151
  ? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
133
- : color.number === bgcolor.number;
152
+ : color.number !== undefined ? color.number === bgcolor.number : color.name === bgcolor.name;
134
153
  }
135
154
  /**
136
155
  * The powerline set: U+E0B0 (right-arrow) divided by U+E0B1 (thin right-arrow),
@@ -275,17 +294,17 @@ export class GradientJoiner {
275
294
  join(left, right) {
276
295
  // [LAW:dataflow-not-control-flow] Endpoints have no opposite anchor to
277
296
  // interpolate toward — the data (a missing neighbor) makes the gradient
278
- // empty. Same for edges lacking a bgcolor: nothing to blend between.
297
+ // empty. Same for edges with nothing to paint: nothing to blend between.
279
298
  if (left === null || right === null)
280
299
  return EMPTY;
281
300
  const steps = this._steps;
282
301
  return deferred(function* (options) {
283
- const lbg = left.edgeStyle("right", options).bgcolor;
284
- const rbg = right.edgeStyle("left", options).bgcolor;
302
+ const lbg = paintableBg(left.edgeStyle("right", options).bgcolor);
303
+ const rbg = paintableBg(right.edgeStyle("left", options).bgcolor);
285
304
  if (!lbg || !rbg)
286
305
  return;
287
- const lTrip = lbg.getTruecolor();
288
- const rTrip = rbg.getTruecolor();
306
+ const from = drawnGround(lbg);
307
+ const to = drawnGround(rbg);
289
308
  // An ASCII cell carries one sample, the background: the gradient keeps
290
309
  // its colours at half the resolution.
291
310
  const half = drawable(options, HALF_BLOCK, " ");
@@ -296,8 +315,10 @@ export class GradientJoiner {
296
315
  for (let i = 0; i < steps; i++) {
297
316
  const tLeft = (2 * i + 0.5) / samples;
298
317
  const tRight = (2 * i + 1.5) / samples;
299
- const fg = ColorSpec.fromRgba(blendRgb(lTrip, rTrip, tLeft));
300
- const bg = ColorSpec.fromRgba(blendRgb(lTrip, rTrip, tRight));
318
+ // Each sample stays a mix of the two grounds, so a theme that draws
319
+ // `blue` in its own shade draws the ramp from that shade.
320
+ const fg = ColorSpec.blend(from, to, tLeft);
321
+ const bg = ColorSpec.blend(from, to, tRight);
301
322
  yield new Segment(half, new Style({ color: fg, bgcolor: bg }));
302
323
  }
303
324
  });