@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
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * Columns — arranges renderables in a multi-column layout.
3
+ *
4
+ * A port of Rich's `Columns`. What it decides is how many columns, and which
5
+ * item goes in which cell. Everything after that — how wide each column is, how
6
+ * far apart, what `expand` does, how rows are separated — is the grid's: the
7
+ * items are laid out as a `Table.grid`, as the reference lays them out.
8
+ * [LAW:one-source-of-truth] A second copy of that geometry here is how the two
9
+ * drifted: an empty item's column was drawn with no width where the grid draws one cell.
3
10
  */
4
- import { Segment } from "../core/segment.js";
11
+ import type { Segment } from "../core/segment.js";
5
12
  import type { PaddingDimensions } from "./padding.js";
6
13
  import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
14
  export interface ColumnsOptions {
@@ -17,45 +24,54 @@ export declare class Columns implements Renderable, Measurable {
17
24
  readonly equal: boolean;
18
25
  readonly colWidth: number | undefined;
19
26
  readonly columnFirst: boolean;
20
- readonly gutterWidth: number;
27
+ private readonly padding;
21
28
  constructor(items?: Iterable<unknown>, options?: ColumnsOptions);
22
29
  render(rawOptions: RenderOptions): Iterable<Segment>;
30
+ measure(rawOptions: RenderOptions): {
31
+ minimum: number;
32
+ maximum: number;
33
+ };
23
34
  /**
24
- * The requested width divided into columns, once.
35
+ * The items at the width offered, as the reference's grid: `_layout`'s
36
+ * cells in a `Table.grid`.
37
+ *
38
+ * [LAW:single-enforcer] `render` draws this and `measure` reports its width,
39
+ * so the two cannot disagree about how wide a Columns is.
25
40
  *
26
- * [LAW:single-enforcer] The three modes disagree only about how many columns
27
- * there are and how wide each one is, so they disagree in one place. `render`
28
- * lays out against this division and `_naturalWidth` is the width at which it
29
- * comes out as one row of every item.
41
+ * A declared `width` is each column's natural width rather than a Table
42
+ * column `width`, which this port never stretches: the reference's grid
43
+ * stretches a Columns' declared width under `expand` like any other.
44
+ * `equal` holds every item to the widest, Rich's `Constrain`; without it
45
+ * the cap is `undefined` and holds nothing.
30
46
  */
31
- private _divide;
47
+ private _grid;
32
48
  /**
33
- * The widest any one item wants to be.
49
+ * How many columns fit the width offered, and which item goes in each cell,
50
+ * decided the way Rich's `Columns` decides them.
34
51
  *
35
- * An item that cannot measure itself wants the offer, which is what `Panel`,
36
- * `Padding`, `Layout` and `Tree` all answer for the same case — and under an
37
- * unbounded offer that is `Infinity`, so `withBoundedWidth` throws and says
38
- * the request was unanswerable. Counting it as one cell instead reported a
39
- * natural width of 1, which resolved an unbounded offer to a single column and
40
- * cropped forty cells of content down to `"x"` with no error at all.
52
+ * Every item carries a width. It is what the item measures, or the declared
53
+ * `width` for all of them. `equal` counts every item as the widest when
54
+ * choosing how many columns fit, and the grid still sizes each column by
55
+ * what is in it — which is why an `equal` layout is not a grid of equal
56
+ * columns, in the reference or here.
57
+ *
58
+ * The count starts at one column per item and drops whenever the columns
59
+ * filled so far, and the gaps between them, pass the width offered. Two
60
+ * columns stand the wider of the padding's left and right sides apart, which
61
+ * is what the collapsed grid leaves between them. One departure from the
62
+ * reference: a declared `width` is searched the same way rather than by
63
+ * `max_width // (width + gap)`, which laid out empty trailing columns and
64
+ * charged the last column a gap it does not have.
41
65
  */
42
- private _itemWidth;
66
+ private _layout;
43
67
  /**
44
68
  * The declared column width, bounded by the width offered.
45
69
  *
46
- * [LAW:single-enforcer] A declared width is what a column asks for, not what
47
- * it takes — the contract `Table._outerWidth` keeps for a declared table
48
- * width. Read by `_divide` for render and by `_naturalWidth` for measure, so
49
- * the two answer from one number: laid out at the raw declared width, a
50
- * six-cell column offered three emitted six-cell lines while `measure`
51
- * reported three, and the terminal's soft wrap took the frame of everything
52
- * printed after it.
70
+ * A declared width is what a column asks for, not what it takes — the
71
+ * contract `Table._outerWidth` keeps for a declared table width. Laid out at
72
+ * the raw declared width, a six-cell column offered three emitted six-cell
73
+ * lines while `measure` reported three, and the terminal's soft wrap took the
74
+ * frame of everything printed after it.
53
75
  */
54
76
  private _declaredWidth;
55
- /** The width at which every item sits on one row: n columns and the gutters between them. */
56
- private _naturalWidth;
57
- measure(rawOptions: RenderOptions): {
58
- minimum: number;
59
- maximum: number;
60
- };
61
77
  }
@@ -1,18 +1,47 @@
1
1
  /**
2
2
  * Columns — arranges renderables in a multi-column layout.
3
+ *
4
+ * A port of Rich's `Columns`. What it decides is how many columns, and which
5
+ * item goes in which cell. Everything after that — how wide each column is, how
6
+ * far apart, what `expand` does, how rows are separated — is the grid's: the
7
+ * items are laid out as a `Table.grid`, as the reference lays them out.
8
+ * [LAW:one-source-of-truth] A second copy of that geometry here is how the two
9
+ * drifted: an empty item's column was drawn with no width where the grid draws one cell.
3
10
  */
4
- import { Segment } from "../core/segment.js";
5
11
  import { Measurement } from "../core/measure.js";
6
- import { isMeasurable, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
12
+ import { normalizePadding } from "./padding.js";
13
+ import { withBoundedWidth, withCellWidth } from "../core/protocol.js";
7
14
  import { cellCount } from "../core/cells.js";
8
- import { embed } from "./embed.js";
15
+ import { EmbeddedText, embed } from "./embed.js";
16
+ import { Constrain } from "./constrain.js";
17
+ import { Table } from "./table.js";
18
+ /**
19
+ * The item in slot `slot` of the grid for `count` columns, counting row by row;
20
+ * `undefined` is a slot no item fills. Constant time, so a search that stops at
21
+ * the first slot that overflows pays for no slot after it.
22
+ */
23
+ function slotItem(slot, items, count, columnFirst) {
24
+ const row = Math.floor(slot / count);
25
+ const col = slot % count;
26
+ // Column-first fills each column top to bottom, and the columns that take one
27
+ // item more than the rest are the leading ones — Rich's `column_lengths`.
28
+ const base = Math.floor(items / count);
29
+ const extra = items % count;
30
+ const length = base + (col < extra ? 1 : 0);
31
+ const index = !columnFirst ? slot : row < length ? col * base + Math.min(col, extra) + row : items;
32
+ return index < items ? index : undefined;
33
+ }
34
+ /** How many slots the grid for `count` columns has: every row full, the last one padded. */
35
+ function slotCount(items, count) {
36
+ return Math.ceil(items / count) * count;
37
+ }
9
38
  export class Columns {
10
39
  renderables;
11
40
  expand;
12
41
  equal;
13
42
  colWidth;
14
43
  columnFirst;
15
- gutterWidth;
44
+ padding;
16
45
  constructor(items, options) {
17
46
  this.renderables = items ? [...items].map(embed) : [];
18
47
  this.expand = options?.expand ?? false;
@@ -26,127 +55,137 @@ export class Columns {
26
55
  // read it as a declared zero.
27
56
  this.colWidth = options?.width === undefined ? undefined : cellCount(options.width);
28
57
  this.columnFirst = options?.columnFirst ?? false;
29
- this.gutterWidth = 2; // default gutter between columns
58
+ // [LAW:parse-dont-validate] Rich's default is `(0, 1)`: one cell between
59
+ // columns and none between rows.
60
+ this.padding = normalizePadding(options?.padding ?? [0, 1]);
30
61
  }
31
62
  *render(rawOptions) {
32
- const items = this.renderables;
33
- if (items.length === 0)
63
+ if (this.renderables.length === 0)
34
64
  return;
35
- // The parsed options, not just a parsed local: `_divide` hands them to
65
+ // The parsed options, not just a parsed local: `_layout` hands them to
36
66
  // `Measurement.get`, and a raw NaN reaching it comes back as a NaN item
37
- // width, then a NaN column count, then `Invalid array length` out of
38
- // `new Array(numCols)` before a single column is laid out. An unbounded
39
- // width reaches the same `new Array` by the same route, which is why the
40
- // parse here is the bounded one.
67
+ // width, then a NaN column count, then `Invalid array length` before a
68
+ // single column is laid out. An unbounded width reaches the same place by
69
+ // the same route, which is why the parse here is the bounded one.
41
70
  const options = withBoundedWidth(rawOptions, this);
42
- const { numCols, colWidths } = this._divide(options);
43
- // Layout items into rows
44
- const numRows = Math.ceil(items.length / numCols);
45
- // [LAW:types-are-the-program] The element type is Renderable — any number
46
- // of lines. Each grid cell renders to its full set of lines; an out-of-range
47
- // slot (the last row is rarely full) is an empty cell. mergeHorizontal then
48
- // stacks every row of every cell line-by-line, so multi-line children
49
- // (Panels, Tables) compose instead of being truncated to their first row.
50
- for (let row = 0; row < numRows; row++) {
51
- const cells = colWidths.map((width, col) => {
52
- const idx = this.columnFirst ? col * numRows + row : row * numCols + col;
53
- const item = items[idx];
54
- const lines = item === undefined
55
- ? []
56
- : Segment.splitLines([
57
- ...item.render({ ...options, maxWidth: width, height: stackedHeight(options.height) }),
58
- ]);
59
- return { lines, width };
60
- });
61
- yield* Segment.mergeHorizontal(cells, this.gutterWidth);
62
- }
71
+ yield* this._grid(options).render(options);
72
+ }
73
+ measure(rawOptions) {
74
+ // The maximum is the grid's, which reports its width before `expand`: a
75
+ // stretch is what a Columns does with a width it is given, not a width it
76
+ // asks for. Reported as the offer instead, `Panel` in fit mode drew a
77
+ // 40-cell frame around five cells of content and an unbounded offer came
78
+ // back unbounded.
79
+ //
80
+ // A floor of one cell is what Columns asks for, since it can always fall
81
+ // back to one column, and the ceiling wins: a bare `minimum: 1` measured
82
+ // into no width at all reported the range 1..0 — a floor above its own
83
+ // ceiling, which the parent that asked cannot divide.
84
+ const parsed = withCellWidth(rawOptions);
85
+ if (this.renderables.length === 0)
86
+ return { minimum: 0, maximum: 0 };
87
+ const { maximum } = this._grid(parsed).measure(parsed);
88
+ return { minimum: Math.min(1, maximum), maximum };
63
89
  }
64
90
  /**
65
- * The requested width divided into columns, once.
91
+ * The items at the width offered, as the reference's grid: `_layout`'s
92
+ * cells in a `Table.grid`.
93
+ *
94
+ * [LAW:single-enforcer] `render` draws this and `measure` reports its width,
95
+ * so the two cannot disagree about how wide a Columns is.
66
96
  *
67
- * [LAW:single-enforcer] The three modes disagree only about how many columns
68
- * there are and how wide each one is, so they disagree in one place. `render`
69
- * lays out against this division and `_naturalWidth` is the width at which it
70
- * comes out as one row of every item.
97
+ * A declared `width` is each column's natural width rather than a Table
98
+ * column `width`, which this port never stretches: the reference's grid
99
+ * stretches a Columns' declared width under `expand` like any other.
100
+ * `equal` holds every item to the widest, Rich's `Constrain`; without it
101
+ * the cap is `undefined` and holds nothing.
71
102
  */
72
- _divide(options) {
73
- const maxWidth = options.maxWidth;
74
- const gutter = this.gutterWidth;
103
+ _grid(options) {
104
+ const { columns, rows, widest } = this._layout(options);
105
+ const grid = Table.grid({ padding: this.padding, expand: this.expand });
75
106
  const declared = this._declaredWidth(options);
76
- if (declared !== undefined) {
77
- const numCols = Math.max(1, Math.floor((maxWidth + gutter) / (declared + gutter)));
78
- return { numCols, colWidths: new Array(numCols).fill(declared) };
107
+ for (let col = 0; col < columns; col++) {
108
+ grid.addColumn(undefined, { minWidth: declared, maxWidth: declared });
79
109
  }
80
- if (this.equal) {
81
- const itemWidth = this._itemWidth(options);
82
- const numCols = Math.max(1, Math.floor((maxWidth + gutter) / (itemWidth + gutter)));
83
- const equalWidth = Math.floor((maxWidth - gutter * (numCols - 1)) / numCols);
84
- return { numCols, colWidths: new Array(numCols).fill(equalWidth) };
110
+ const cap = this.equal ? widest : undefined;
111
+ // A string item is read here, under the options Columns is drawn with,
112
+ // before the grid gets it: Rich's `Columns` reads its strings through
113
+ // `render_str`, so they are highlighted where a table cell's are not.
114
+ const items = this.renderables.map((item) => (item instanceof EmbeddedText ? item.text(options) : item));
115
+ for (const row of rows) {
116
+ grid.addRow(...row.map((index) => (index === undefined ? "" : new Constrain(items[index], cap))));
85
117
  }
86
- // Auto: as many columns as the widest item fits into, capped at the item
87
- // count. Sized from the content rather than from a flat four cells per
88
- // column, because `_naturalWidth` below is the inverse of *this* expression
89
- // and a constant has no inverse: six two-cell items reported a natural
90
- // width of 22 and then wrapped into two rows at 22, since `floor(22 / 4)`
91
- // is five columns no matter how narrow the items are.
92
- const itemWidth = this._itemWidth(options);
93
- const numCols = Math.min(this.renderables.length, Math.max(1, Math.floor((maxWidth + gutter) / (itemWidth + gutter))));
94
- const colW = Math.floor((maxWidth - gutter * (numCols - 1)) / numCols);
95
- return { numCols, colWidths: new Array(numCols).fill(colW) };
118
+ return grid;
96
119
  }
97
120
  /**
98
- * The widest any one item wants to be.
121
+ * How many columns fit the width offered, and which item goes in each cell,
122
+ * decided the way Rich's `Columns` decides them.
99
123
  *
100
- * An item that cannot measure itself wants the offer, which is what `Panel`,
101
- * `Padding`, `Layout` and `Tree` all answer for the same case — and under an
102
- * unbounded offer that is `Infinity`, so `withBoundedWidth` throws and says
103
- * the request was unanswerable. Counting it as one cell instead reported a
104
- * natural width of 1, which resolved an unbounded offer to a single column and
105
- * cropped forty cells of content down to `"x"` with no error at all.
124
+ * Every item carries a width. It is what the item measures, or the declared
125
+ * `width` for all of them. `equal` counts every item as the widest when
126
+ * choosing how many columns fit, and the grid still sizes each column by
127
+ * what is in it — which is why an `equal` layout is not a grid of equal
128
+ * columns, in the reference or here.
129
+ *
130
+ * The count starts at one column per item and drops whenever the columns
131
+ * filled so far, and the gaps between them, pass the width offered. Two
132
+ * columns stand the wider of the padding's left and right sides apart, which
133
+ * is what the collapsed grid leaves between them. One departure from the
134
+ * reference: a declared `width` is searched the same way rather than by
135
+ * `max_width // (width + gap)`, which laid out empty trailing columns and
136
+ * charged the last column a gap it does not have.
106
137
  */
107
- _itemWidth(options) {
108
- let widest = 1;
109
- for (const item of this.renderables) {
110
- widest = Math.max(widest, isMeasurable(item)
111
- ? Measurement.get(options, item).maximum
112
- : options.maxWidth);
113
- }
114
- return widest;
138
+ _layout(options) {
139
+ const count = this.renderables.length;
140
+ const maxWidth = options.maxWidth;
141
+ const [, right, , left] = this.padding;
142
+ const gap = Math.max(left, right);
143
+ const declared = this._declaredWidth(options);
144
+ // An item that cannot measure itself wants the whole offer. Counting it as one
145
+ // cell instead resolved an unbounded offer to a single column and cropped
146
+ // forty cells of content down to `"x"` with no error at all.
147
+ const sizes = this.renderables.map((item) => declared ?? Measurement.get(options, item).maximum);
148
+ // A fold, not `Math.max(...sizes)`: spreading one argument per item
149
+ // overflows the call stack at a few hundred thousand items.
150
+ const widest = sizes.reduce((w, size) => Math.max(w, size), 0);
151
+ const fits = this.equal ? sizes.map(() => widest) : sizes;
152
+ // One attempt either fits in the offer or names the smaller count to try
153
+ // next. `total` is the columns so far and the gaps between them, kept as
154
+ // the widths grow so that a slot costs the same however many columns came
155
+ // before it.
156
+ const attempt = (columns) => {
157
+ const widths = [];
158
+ let total = 0;
159
+ for (let slot = 0; slot < slotCount(count, columns); slot++) {
160
+ const col = slot % columns;
161
+ const index = slotItem(slot, count, columns, this.columnFirst);
162
+ const held = widths[col];
163
+ const width = Math.max(held ?? 0, index === undefined ? 0 : fits[index]);
164
+ total += width - (held ?? 0) + (held === undefined && col > 0 ? gap : 0);
165
+ widths[col] = width;
166
+ // Never zero: no item measures wider than the offer, so the first
167
+ // column alone always fits.
168
+ if (total > maxWidth)
169
+ return widths.length - 1;
170
+ }
171
+ return columns;
172
+ };
173
+ let columns = count;
174
+ for (let next = attempt(columns); next < columns; next = attempt(columns))
175
+ columns = next;
176
+ const rows = Array.from({ length: slotCount(count, columns) / columns }, (_, row) => Array.from({ length: columns }, (_, col) => slotItem(row * columns + col, count, columns, this.columnFirst)));
177
+ return { columns, rows, widest };
115
178
  }
116
179
  /**
117
180
  * The declared column width, bounded by the width offered.
118
181
  *
119
- * [LAW:single-enforcer] A declared width is what a column asks for, not what
120
- * it takes — the contract `Table._outerWidth` keeps for a declared table
121
- * width. Read by `_divide` for render and by `_naturalWidth` for measure, so
122
- * the two answer from one number: laid out at the raw declared width, a
123
- * six-cell column offered three emitted six-cell lines while `measure`
124
- * reported three, and the terminal's soft wrap took the frame of everything
125
- * printed after it.
182
+ * A declared width is what a column asks for, not what it takes — the
183
+ * contract `Table._outerWidth` keeps for a declared table width. Laid out at
184
+ * the raw declared width, a six-cell column offered three emitted six-cell
185
+ * lines while `measure` reported three, and the terminal's soft wrap took the
186
+ * frame of everything printed after it.
126
187
  */
127
188
  _declaredWidth(options) {
128
189
  return this.colWidth === undefined ? undefined : Math.min(this.colWidth, options.maxWidth);
129
190
  }
130
- /** The width at which every item sits on one row: n columns and the gutters between them. */
131
- _naturalWidth(options) {
132
- const count = this.renderables.length;
133
- if (count === 0)
134
- return 0;
135
- const width = this._declaredWidth(options) ?? this._itemWidth(options);
136
- return count * width + this.gutterWidth * (count - 1);
137
- }
138
- measure(rawOptions) {
139
- // A floor of one cell is what Columns asks for; the ceiling is what it was
140
- // offered, and the ceiling wins. Stated as a bare `minimum: 1`, a Columns
141
- // measured into no width at all reported the range 1..0 — a floor above
142
- // its own ceiling, which the parent that asked cannot divide.
143
- //
144
- // The ceiling is not the whole answer, though: reported as the offer alone,
145
- // a Columns claimed every cell it was shown, so `Panel` in fit mode drew a
146
- // 40-cell frame around five cells of content and an unbounded offer came
147
- // back unbounded.
148
- const parsed = withCellWidth(rawOptions);
149
- const maximum = Math.min(this._naturalWidth(parsed), parsed.maxWidth);
150
- return { minimum: Math.min(1, maximum), maximum };
151
- }
152
191
  }
@@ -2,7 +2,6 @@
2
2
  * Constrain — wraps a renderable and constrains its maximum width.
3
3
  */
4
4
  import { Measurement } from "../core/measure.js";
5
- import { isMeasurable } from "../core/protocol.js";
6
5
  export class Constrain {
7
6
  renderable;
8
7
  width;
@@ -22,16 +21,11 @@ export class Constrain {
22
21
  yield* this.renderable.render(innerOptions);
23
22
  }
24
23
  measure(options) {
25
- if (isMeasurable(this.renderable)) {
26
- const measurement = Measurement.get(options, this.renderable);
27
- const maxWidth = this.width !== undefined
28
- ? Math.min(this.width, options.maxWidth)
29
- : options.maxWidth;
30
- return {
31
- minimum: measurement.minimum,
32
- maximum: Math.min(measurement.maximum, maxWidth),
33
- };
34
- }
35
- return { minimum: 1, maximum: options.maxWidth };
24
+ // Measured at the width it will be drawn at, as Rich's `update_width` does;
25
+ // `Measurement.get` then holds the answer inside that offer.
26
+ const maxWidth = this.width !== undefined
27
+ ? Math.min(this.width, options.maxWidth)
28
+ : options.maxWidth;
29
+ return Measurement.get({ ...options, maxWidth }, this.renderable);
36
30
  }
37
31
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Effected — a renderable whose finished cells' colours pass through an
3
+ * effect at a moment `t`.
4
+ *
5
+ * An effect is one pure function: a cell's colours, where the cell is, and a
6
+ * time in, the cell's new colours out. Shimmer, pulse, fade and dissolve are
7
+ * all this one shape, differing by what the function does with its inputs
8
+ * [LAW:one-type-per-behavior]; two effects stack by nesting one wrapper in
9
+ * another, which is the same as calling one after the other.
10
+ *
11
+ * [LAW:no-ambient-temporal-coupling] `t` is a constructor argument, not a
12
+ * clock read. Whoever draws frames builds the view for each frame's `t`, so
13
+ * the same `t` always draws the same bytes, at 30 frames a second or one
14
+ * every two seconds.
15
+ *
16
+ * What an effect may not do is the contract this module holds:
17
+ *
18
+ * - It changes how cells look, never which cells exist or who drew them. The
19
+ * child's text is walked by grapheme cluster, so a wide glyph is one cell
20
+ * to the effect and is never cut, and a run cut out of a segment keeps that
21
+ * segment's link and meta and carries its anchor shifted to the run's first
22
+ * cell (`Style.shiftedBy`), so hit testing still finds the owner.
23
+ * - A cell whose colours come back unchanged keeps its original `ColorSpec`,
24
+ * and a segment whose every cell is unchanged is yielded as it was, so the
25
+ * identity effect is byte-identical. Neighbouring cells given equal colours
26
+ * stay one segment: a segment is cut only where the colours change.
27
+ * - New colours are built with `ColorSpec.fromRgba`, never parsed from a
28
+ * string, so no frame adds an entry to a parse cache.
29
+ * - With no colour output (`colorSystem: null`) there is nothing for an
30
+ * effect to change, and the child's segments pass through untouched.
31
+ *
32
+ * Positions are the child's own: row and column count from the top-left of
33
+ * what the child drew, so a sweep crosses the element, not the screen.
34
+ */
35
+ import { type ColorRgba, type TerminalTheme } from "../core/color.js";
36
+ import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
37
+ import { Segment } from "../core/segment.js";
38
+ /**
39
+ * A cell's foreground and background as they appear on screen — the glyph's
40
+ * colour and the colour behind it — which are the colours an effect moves.
41
+ * Under `reverse` they are the style's two colours swapped.
42
+ */
43
+ export interface CellColors {
44
+ readonly fg: ColorRgba;
45
+ readonly bg: ColorRgba;
46
+ }
47
+ /**
48
+ * Where a cell is in the output of the renderable an effect wraps, and its
49
+ * seed: a number in [0, 1) that is the same for that cell on every frame, for
50
+ * an effect that varies cell by cell (a per-cell offset in time, a sparkle).
51
+ */
52
+ export interface EffectCell {
53
+ readonly row: number;
54
+ readonly col: number;
55
+ readonly seed: number;
56
+ }
57
+ /** A cell's colours at time `t`, in seconds. Return `colors` to leave it be. */
58
+ export type Effect = (colors: CellColors, cell: EffectCell, t: number) => CellColors;
59
+ export interface EffectedOptions {
60
+ /** The moment to draw, in seconds. */
61
+ readonly t: number;
62
+ /**
63
+ * Names the element for its seeds, so two elements showing the same text do
64
+ * not twinkle in lockstep. The same key gives the same seeds on every frame.
65
+ */
66
+ readonly key: string;
67
+ /**
68
+ * The terminal the frame is drawn on: the colours a cell that sets none is
69
+ * drawn in, and the sixteen an ANSI colour names. The terminal's own palette
70
+ * is never queried, so the caller says what it is.
71
+ */
72
+ readonly theme: TerminalTheme;
73
+ }
74
+ export declare class Effected implements Renderable, Measurable {
75
+ readonly renderable: Renderable;
76
+ readonly effect: Effect;
77
+ readonly t: number;
78
+ readonly key: string;
79
+ readonly theme: TerminalTheme;
80
+ constructor(renderable: Renderable, effect: Effect, options: EffectedOptions);
81
+ render(options: RenderOptions): Iterable<Segment>;
82
+ measure(options: RenderOptions): {
83
+ minimum: number;
84
+ maximum: number;
85
+ };
86
+ /**
87
+ * The style that draws a run's slot colours `to` where the segment drew
88
+ * `from` with `wire`: a slot the effect left alone keeps its written spec,
89
+ * and a moved one is laid on what is beneath it — the ground on
90
+ * `SURFACE_BLACK`, the glyph on that ground, as the writer lays any
91
+ * translucent colour (`Style.drawnColors`) — then drawn in the colour nearest it at `depth`
92
+ * that the terminal shows (`ColorSpec.matchOn`), so a colour moved a little
93
+ * off a theme slot or the default colour is still drawn as that one.
94
+ */
95
+ private respec;
96
+ /** A cell's seed: FNV-1a of the key and the position, scaled into [0, 1). */
97
+ private seed;
98
+ }