@promptctl/rich-js 0.24.1 → 0.25.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 (156) hide show
  1. package/README.md +7 -0
  2. package/dist/core/anchor.d.ts +5 -0
  3. package/dist/core/anchor.js +11 -4
  4. package/dist/core/animation.d.ts +175 -10
  5. package/dist/core/animation.js +283 -15
  6. package/dist/core/ansi.js +2 -2
  7. package/dist/core/box.d.ts +7 -7
  8. package/dist/core/cells.d.ts +23 -13
  9. package/dist/core/cells.js +58 -48
  10. package/dist/core/clock.d.ts +2 -2
  11. package/dist/core/clock.js +6 -4
  12. package/dist/core/color.d.ts +27 -10
  13. package/dist/core/color.js +21 -29
  14. package/dist/core/console.d.ts +28 -17
  15. package/dist/core/console.js +44 -39
  16. package/dist/core/easing.d.ts +30 -0
  17. package/dist/core/easing.js +65 -20
  18. package/dist/core/emoji.d.ts +1 -1
  19. package/dist/core/emoji.js +1 -2
  20. package/dist/core/export-html.js +2 -1
  21. package/dist/core/export-lines.js +3 -3
  22. package/dist/core/highlighter.d.ts +9 -8
  23. package/dist/core/highlighter.js +3 -2
  24. package/dist/core/json.js +2 -2
  25. package/dist/core/markup.js +16 -12
  26. package/dist/core/measure.js +2 -2
  27. package/dist/core/memo.d.ts +11 -2
  28. package/dist/core/memo.js +16 -0
  29. package/dist/core/oklch.js +17 -6
  30. package/dist/core/osc8.d.ts +1 -1
  31. package/dist/core/osc8.js +4 -5
  32. package/dist/core/paint.d.ts +2 -1
  33. package/dist/core/paint.js +4 -3
  34. package/dist/core/pattern.d.ts +21 -0
  35. package/dist/core/pattern.js +21 -0
  36. package/dist/core/pretty.js +9 -10
  37. package/dist/core/protocol.d.ts +100 -39
  38. package/dist/core/protocol.js +167 -21
  39. package/dist/core/render.d.ts +15 -2
  40. package/dist/core/render.js +25 -5
  41. package/dist/core/segment.d.ts +13 -5
  42. package/dist/core/segment.js +23 -9
  43. package/dist/core/spinnerData.d.ts +1 -1
  44. package/dist/core/state.d.ts +53 -0
  45. package/dist/core/state.js +82 -0
  46. package/dist/core/strip.d.ts +13 -16
  47. package/dist/core/strip.js +119 -110
  48. package/dist/core/style.d.ts +47 -14
  49. package/dist/core/style.js +57 -28
  50. package/dist/core/text.d.ts +5 -8
  51. package/dist/core/text.js +116 -124
  52. package/dist/core/timeline.d.ts +30 -5
  53. package/dist/core/timeline.js +43 -7
  54. package/dist/core/tween.d.ts +1 -1
  55. package/dist/core/tween.js +5 -3
  56. package/dist/core/view.d.ts +92 -0
  57. package/dist/core/view.js +83 -0
  58. package/dist/host/app.d.ts +49 -21
  59. package/dist/host/app.js +71 -37
  60. package/dist/host/input.js +3 -4
  61. package/dist/index.d.ts +24 -12
  62. package/dist/index.js +9 -4
  63. package/dist/node/traceback.js +4 -3
  64. package/dist/renderables/align.d.ts +2 -3
  65. package/dist/renderables/align.js +0 -3
  66. package/dist/renderables/change.d.ts +42 -0
  67. package/dist/renderables/change.js +112 -0
  68. package/dist/renderables/columns.d.ts +3 -4
  69. package/dist/renderables/columns.js +2 -7
  70. package/dist/renderables/constrain.d.ts +2 -3
  71. package/dist/renderables/constrain.js +0 -3
  72. package/dist/renderables/effect.d.ts +132 -54
  73. package/dist/renderables/effect.js +188 -115
  74. package/dist/renderables/effects.d.ts +122 -11
  75. package/dist/renderables/effects.js +238 -94
  76. package/dist/renderables/embed.d.ts +1 -1
  77. package/dist/renderables/embed.js +16 -8
  78. package/dist/renderables/flexStrip.d.ts +2 -3
  79. package/dist/renderables/flexStrip.js +0 -4
  80. package/dist/renderables/frames.d.ts +35 -54
  81. package/dist/renderables/frames.js +43 -58
  82. package/dist/renderables/glyph-effect.d.ts +158 -0
  83. package/dist/renderables/glyph-effect.js +237 -0
  84. package/dist/renderables/glyph-effects.d.ts +147 -0
  85. package/dist/renderables/glyph-effects.js +283 -0
  86. package/dist/renderables/group.d.ts +3 -4
  87. package/dist/renderables/group.js +0 -3
  88. package/dist/renderables/layout.d.ts +12 -12
  89. package/dist/renderables/layout.js +40 -46
  90. package/dist/renderables/lifecycle.d.ts +77 -0
  91. package/dist/renderables/lifecycle.js +132 -0
  92. package/dist/renderables/list-view.d.ts +4 -5
  93. package/dist/renderables/list-view.js +9 -13
  94. package/dist/renderables/live.d.ts +36 -21
  95. package/dist/renderables/live.js +88 -41
  96. package/dist/renderables/markdown-parse.d.ts +4 -4
  97. package/dist/renderables/markdown-parse.js +52 -46
  98. package/dist/renderables/markdown-url.js +11 -11
  99. package/dist/renderables/padding.d.ts +4 -5
  100. package/dist/renderables/padding.js +0 -3
  101. package/dist/renderables/panel.d.ts +14 -16
  102. package/dist/renderables/panel.js +6 -14
  103. package/dist/renderables/progress.d.ts +83 -34
  104. package/dist/renderables/progress.js +182 -95
  105. package/dist/renderables/progressBar.d.ts +22 -12
  106. package/dist/renderables/progressBar.js +32 -9
  107. package/dist/renderables/prompt.js +3 -2
  108. package/dist/renderables/python-space.js +2 -1
  109. package/dist/renderables/scroll.d.ts +50 -0
  110. package/dist/renderables/scroll.js +106 -0
  111. package/dist/renderables/spinner.d.ts +35 -14
  112. package/dist/renderables/spinner.js +65 -25
  113. package/dist/renderables/status.js +1 -1
  114. package/dist/renderables/syntax.d.ts +10 -9
  115. package/dist/renderables/syntax.js +3 -2
  116. package/dist/renderables/table.d.ts +34 -40
  117. package/dist/renderables/table.js +41 -63
  118. package/dist/renderables/traceback.d.ts +2 -2
  119. package/dist/renderables/traceback.js +6 -5
  120. package/dist/renderables/tree.d.ts +27 -25
  121. package/dist/renderables/tree.js +104 -58
  122. package/dist/renderables/viewport.d.ts +75 -44
  123. package/dist/renderables/viewport.js +145 -82
  124. package/dist/template-bindings/animation-funcs.d.ts +78 -9
  125. package/dist/template-bindings/animation-funcs.js +132 -14
  126. package/dist/template-bindings/index.d.ts +5 -5
  127. package/dist/template-bindings/index.js +5 -5
  128. package/dist/themes/ramp.d.ts +46 -3
  129. package/dist/themes/ramp.js +61 -4
  130. package/dist/themes/registry.js +5 -7
  131. package/dist/widgets/button.js +3 -2
  132. package/dist/widgets/checkbox.js +4 -3
  133. package/dist/widgets/dropdown.d.ts +3 -1
  134. package/dist/widgets/dropdown.js +20 -15
  135. package/dist/widgets/event-router.js +4 -2
  136. package/dist/widgets/focus-manager.d.ts +2 -0
  137. package/dist/widgets/focus-manager.js +6 -4
  138. package/dist/widgets/hit.d.ts +6 -0
  139. package/dist/widgets/hit.js +8 -1
  140. package/dist/widgets/observable.d.ts +23 -0
  141. package/dist/widgets/observable.js +43 -0
  142. package/dist/widgets/slider.d.ts +1 -1
  143. package/dist/widgets/slider.js +12 -3
  144. package/dist/widgets/static-item.d.ts +2 -3
  145. package/dist/widgets/static-item.js +0 -3
  146. package/dist/widgets/text-input.d.ts +28 -32
  147. package/dist/widgets/text-input.js +360 -346
  148. package/dist/widgets/themed-widget.js +2 -1
  149. package/dist/widgets/toggle.js +4 -3
  150. package/dist/widgets/types.d.ts +10 -4
  151. package/dist/widgets/types.js +8 -2
  152. package/dist/widgets/widget-app.d.ts +14 -4
  153. package/dist/widgets/widget-app.js +36 -7
  154. package/dist/widgets/widget-base.d.ts +2 -2
  155. package/dist/widgets/widget-base.js +29 -18
  156. package/package.json +8 -1
@@ -11,6 +11,7 @@ import { Padding, normalizePadding } from "./padding.js";
11
11
  import { exactWeights, ratioDistribute, ratioReduce } from "./ratio.js";
12
12
  import { getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
13
13
  import { Measurement } from "../core/measure.js";
14
+ import { ListState, State } from "../core/state.js";
14
15
  /** A `want` no budget can satisfy: the column takes every cell its weight earns. */
15
16
  const UNBOUNDED = Number.MAX_SAFE_INTEGER;
16
17
  /**
@@ -256,10 +257,9 @@ export class Column {
256
257
  noWrap;
257
258
  overflow;
258
259
  highlight;
259
- _cells;
260
260
  constructor(options) {
261
- this.header = options?.header;
262
- this.footer = options?.footer;
261
+ this._header = new State(embed(options?.header));
262
+ this._footer = new State(embed(options?.footer));
263
263
  this.headerStyle = options?.headerStyle ?? NULL_STYLE;
264
264
  this.footerStyle = options?.footerStyle ?? NULL_STYLE;
265
265
  this.style = options?.style ?? NULL_STYLE;
@@ -271,7 +271,6 @@ export class Column {
271
271
  this.noWrap = options?.noWrap ?? false;
272
272
  this.overflow = options?.overflow ?? "ellipsis";
273
273
  this.highlight = options?.highlight ?? false;
274
- this._cells = [];
275
274
  }
276
275
  /**
277
276
  * The two stamped cells, stamped on assignment rather than at the constructor.
@@ -291,30 +290,30 @@ export class Column {
291
290
  * which is why the setters take `undefined` rather than defaulting around it.
292
291
  */
293
292
  get header() {
294
- return this._header;
293
+ return this._header.get();
295
294
  }
296
295
  set header(content) {
297
- this._header = embed(content);
296
+ this._header.set(embed(content));
298
297
  }
299
298
  get footer() {
300
- return this._footer;
299
+ return this._footer.get();
301
300
  }
302
301
  set footer(content) {
303
- this._footer = embed(content);
302
+ this._footer.set(embed(content));
304
303
  }
305
304
  get flexible() {
306
305
  return columnShare(this) > 0;
307
306
  }
308
- /** @internal */
309
- addCell(cell) {
310
- this._cells.push(cell);
311
- }
312
- /** @internal */
313
- getCells() {
314
- return this._cells;
315
- }
316
307
  copy() {
317
- const col = new Column({
308
+ return new Column({
309
+ // Shared, not copied, as a row's cells are: `embed` already copied a
310
+ // caller's `RichText`, what it hands back is never changed, and `embed`
311
+ // passes a renderable through.
312
+ header: this.header,
313
+ footer: this.footer,
314
+ headerStyle: this.headerStyle,
315
+ footerStyle: this.footerStyle,
316
+ style: this.style,
318
317
  justify: this.justify,
319
318
  width: this.width,
320
319
  minWidth: this.minWidth,
@@ -324,14 +323,6 @@ export class Column {
324
323
  overflow: this.overflow,
325
324
  highlight: this.highlight,
326
325
  });
327
- col.headerStyle = this.headerStyle;
328
- col.footerStyle = this.footerStyle;
329
- col.style = this.style;
330
- // Shared, not copied, as a row's cells are: `embed` already copied a
331
- // caller's `RichText`, and what it hands back is never changed.
332
- col._header = this._header;
333
- col._footer = this._footer;
334
- return col;
335
326
  }
336
327
  }
337
328
  // A row's cell in column `index`: a column the row is short of draws a blank one.
@@ -339,8 +330,9 @@ function cellAt(row, index) {
339
330
  return row.cells[index] ?? new EmbeddedText(undefined);
340
331
  }
341
332
  export class Table {
342
- _columns;
343
- _rows;
333
+ // Rich's Table grows by addColumn and addRow after it is built.
334
+ _columns = new ListState();
335
+ _rows = new ListState();
344
336
  box;
345
337
  _title;
346
338
  _caption;
@@ -371,8 +363,6 @@ export class Table {
371
363
  return this._caption;
372
364
  }
373
365
  constructor(options) {
374
- this._columns = [];
375
- this._rows = [];
376
366
  this.box = options?.box !== undefined ? options.box : HEAVY_HEAD;
377
367
  // An empty title draws no row, as the reference's `if self.title` draws
378
368
  // none; markup that styles nothing is a title, and draws a blank one.
@@ -405,10 +395,10 @@ export class Table {
405
395
  this.highlight = options?.highlight ?? false;
406
396
  }
407
397
  get columns() {
408
- return this._columns;
398
+ return this._columns.get();
409
399
  }
410
400
  get rowCount() {
411
- return this._rows.length;
401
+ return this._rows.get().length;
412
402
  }
413
403
  addColumn(header, options) {
414
404
  const col = new Column({ ...options, header: header ?? options?.header, highlight: options?.highlight ?? this.highlight });
@@ -437,20 +427,18 @@ export class Table {
437
427
  // behind. [LAW:no-ambient-temporal-coupling]
438
428
  const resolved = cells.map(embed);
439
429
  // Auto-create columns if needed
440
- while (this._columns.length < resolved.length) {
430
+ while (this._columns.get().length < resolved.length) {
441
431
  this.addColumn();
442
432
  }
443
- this._rows.push({ cells: resolved, endSection });
433
+ this._rows.push({ cells: resolved, endSection: new State(endSection) });
444
434
  return this;
445
435
  }
446
436
  addSection() {
447
- if (this._rows.length > 0) {
448
- this._rows[this._rows.length - 1].endSection = true;
449
- }
437
+ this._rows.get().at(-1)?.endSection.set(true);
450
438
  return this;
451
439
  }
452
440
  *render(rawOptions) {
453
- if (this._columns.length === 0) {
441
+ if (this._columns.get().length === 0) {
454
442
  yield Segment.line();
455
443
  return;
456
444
  }
@@ -483,17 +471,17 @@ export class Table {
483
471
  // once per cell, as the reference's `_get_cells` does per column.
484
472
  const headerStyle = getStyle(options, this.headerStyle);
485
473
  const footerStyle = getStyle(options, this.footerStyle);
486
- const columnStyles = this._columns.map((c) => getStyle(options, c.style));
474
+ const columnStyles = this._columns.get().map((c) => getStyle(options, c.style));
487
475
  const stripes = this.rowStyles.map((s) => getStyle(options, s));
488
476
  // Each row's padding above and below, by where it stands among every row
489
477
  // the table draws, header and footer included, as the reference counts them.
490
478
  const firstBody = this.showHeader ? 1 : 0;
491
- const lastRow = firstBody + this._rows.length + (this.showFooter ? 1 : 0) - 1;
479
+ const lastRow = firstBody + this._rows.get().length + (this.showFooter ? 1 : 0) - 1;
492
480
  const rowPadding = (position) => this._rowPadding(position === 0, position === lastRow);
493
481
  // Header row
494
482
  if (this.showHeader) {
495
- const headerCells = this._columns.map((c) => c.header);
496
- const headerStyles = this._columns.map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
483
+ const headerCells = this._columns.get().map((c) => c.header);
484
+ const headerStyles = this._columns.get().map((c) => headerStyle.add(getStyle(options, c.headerStyle)));
497
485
  yield* this._renderRow(options, headerCells, headerStyles, NULL_STYLE, geometry, rowPadding(0), box, "head", border);
498
486
  // Header separator
499
487
  if (box) {
@@ -501,15 +489,15 @@ export class Table {
501
489
  }
502
490
  }
503
491
  // Data rows
504
- for (let rowIdx = 0; rowIdx < this._rows.length; rowIdx++) {
505
- const row = this._rows[rowIdx];
492
+ for (let rowIdx = 0; rowIdx < this._rows.get().length; rowIdx++) {
493
+ const row = this._rows.get()[rowIdx];
506
494
  const rowCells = this._rowCells(row);
507
495
  const rowStyle = stripes[rowIdx % stripes.length] ?? NULL_STYLE;
508
496
  const cellStyles = columnStyles.map((s) => s.add(rowStyle));
509
497
  yield* this._renderRow(options, rowCells, cellStyles, rowStyle, geometry, rowPadding(firstBody + rowIdx), box, "row", border);
510
498
  // Row separator
511
- const showSep = this.showLines || row.endSection;
512
- if (showSep && box && rowIdx < this._rows.length - 1) {
499
+ const showSep = this.showLines || row.endSection.get();
500
+ if (showSep && box && rowIdx < this._rows.get().length - 1) {
513
501
  yield* box.getRow(geometry.cellWidths, "row", border, edge);
514
502
  }
515
503
  }
@@ -518,8 +506,8 @@ export class Table {
518
506
  if (box) {
519
507
  yield* box.getRow(geometry.cellWidths, "foot", border, edge);
520
508
  }
521
- const footerCells = this._columns.map((c) => c.footer);
522
- const footerStyles = this._columns.map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
509
+ const footerCells = this._columns.get().map((c) => c.footer);
510
+ const footerStyles = this._columns.get().map((c) => footerStyle.add(getStyle(options, c.footerStyle)));
523
511
  yield* this._renderRow(options, footerCells, footerStyles, NULL_STYLE, geometry, rowPadding(lastRow), box, "foot", border);
524
512
  }
525
513
  // Bottom border
@@ -644,9 +632,9 @@ export class Table {
644
632
  const frame = this._frame();
645
633
  const inner = {
646
634
  ...options,
647
- maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.length - 1)),
635
+ maxWidth: Math.max(0, outerWidth - frame.edge * 2 - frame.divider * Math.max(0, this._columns.get().length - 1)),
648
636
  };
649
- return this._columns.map((col, index) => {
637
+ return this._columns.get().map((col, index) => {
650
638
  const sizing = this._columnSizing(col, index, inner, this._columnPadding(index));
651
639
  const floor = sizing.want === 0 ? 1 : 0;
652
640
  return { ...sizing, minimum: sizing.minimum + floor, want: sizing.want + floor, floor };
@@ -752,7 +740,7 @@ export class Table {
752
740
  _columnPadding(index) {
753
741
  const [, right, , left] = this.padding;
754
742
  const first = index === 0;
755
- const last = index === this._columns.length - 1;
743
+ const last = index === this._columns.get().length - 1;
756
744
  const collapsed = this.collapsePadding && !first ? Math.max(0, left - right) : left;
757
745
  return [!this.padEdge && first ? 0 : collapsed, !this.padEdge && last ? 0 : right];
758
746
  }
@@ -761,18 +749,8 @@ export class Table {
761
749
  const collapsed = this.collapsePadding && !last ? Math.max(0, top - bottom) : bottom;
762
750
  return [!this.padEdge && first ? 0 : top, !this.padEdge && last ? 0 : collapsed];
763
751
  }
764
- // Every cell `render` draws, in the order it draws them: row by row, the
765
- // header and footer when it shows them.
766
- *contents() {
767
- if (this.showHeader)
768
- yield* this._columns.map((c) => c.header);
769
- for (const row of this._rows)
770
- yield* this._rowCells(row);
771
- if (this.showFooter)
772
- yield* this._columns.map((c) => c.footer);
773
- }
774
752
  _rowCells(row) {
775
- return this._columns.map((_, index) => cellAt(row, index));
753
+ return this._columns.get().map((_, index) => cellAt(row, index));
776
754
  }
777
755
  /**
778
756
  * Every cell column `index` draws, in draw order.
@@ -788,7 +766,7 @@ export class Table {
788
766
  *_columnCells(col, index) {
789
767
  if (this.showHeader)
790
768
  yield col.header;
791
- for (const row of this._rows)
769
+ for (const row of this._rows.get())
792
770
  yield cellAt(row, index);
793
771
  if (this.showFooter)
794
772
  yield col.footer;
@@ -850,7 +828,7 @@ export class Table {
850
828
  // width could not seat are absent from `cellWidths` and so are never
851
829
  // rendered at all.
852
830
  const cellLines = cellWidths.map((cellWidth, index) => {
853
- const col = this._columns[index];
831
+ const col = this._columns.get()[index];
854
832
  const [left, right] = this._columnPadding(index);
855
833
  // [LAW:single-enforcer] The padding on every side is part of the cell,
856
834
  // drawn by `Padding` as the reference's `_get_cells` wraps each cell in
@@ -18,14 +18,14 @@
18
18
  import { Segment } from "../core/segment.js";
19
19
  import { type Renderable, type RenderOptions } from "../core/protocol.js";
20
20
  export interface TracebackOptions {
21
- suppress?: string[];
21
+ suppress?: readonly string[];
22
22
  maxFrames?: number;
23
23
  }
24
24
  export declare class Traceback implements Renderable {
25
25
  /** What was caught, as it was handed in. */
26
26
  readonly error: unknown;
27
27
  readonly maxFrames: number;
28
- readonly suppress: string[];
28
+ readonly suppress: readonly string[];
29
29
  private readonly caught;
30
30
  constructor(error: unknown, options?: TracebackOptions);
31
31
  render(options: RenderOptions): Iterable<Segment>;
@@ -20,6 +20,7 @@ import { RichText } from "../core/text.js";
20
20
  import { Pretty, UNSEEN_DATA_BOUNDS } from "../core/pretty.js";
21
21
  import { render } from "../core/protocol.js";
22
22
  import { paddedContentWidth } from "./padding.js";
23
+ import { pattern } from "../core/pattern.js";
23
24
  /**
24
25
  * [LAW:parse-dont-validate] The checkpoint between whatever a `catch` caught —
25
26
  * JavaScript lets a program throw any value — and the report. Anything with a
@@ -141,13 +142,13 @@ function parseStack(stack, name, message) {
141
142
  * any name, so it is read and dropped; the lookahead keeps `at async (…)` — a
142
143
  * function named `async` — a name.
143
144
  */
144
- const V8_FRAME = /^at\s+(?:async\s+(?!\())?(.*)$/;
145
+ const V8_FRAME = pattern(/^at\s+(?:async\s+(?!\())?(.*)$/);
145
146
  /**
146
147
  * SpiderMonkey and JavaScriptCore write `[fn]@location`, Firefox prefixing an
147
148
  * awaited call's name with a cause up to a `*` (`async*`, `promise callback*`),
148
149
  * which is dropped as V8's `async` is.
149
150
  */
150
- const AT_SIGN_FRAME = /^(?:[^@]*\*)?([^@]*)@(.*)$/;
151
+ const AT_SIGN_FRAME = pattern(/^(?:[^@]*\*)?([^@]*)@(.*)$/);
151
152
  function parseFrame(text) {
152
153
  const v8 = V8_FRAME.exec(text);
153
154
  if (v8) {
@@ -188,9 +189,9 @@ function openingParen(body) {
188
189
  * the code to it (`file line 8 > eval:1:1`).
189
190
  */
190
191
  const LOCATIONS = [
191
- /^eval at [^]*?\(([^()]+):(\d+):\d+\)/,
192
- /^(.*?) line (\d+) > (?:eval|Function)/,
193
- /^(.*?):(\d+)(?::\d+)?$/,
192
+ pattern(/^eval at [^]*?\(([^()]+):(\d+):\d+\)/),
193
+ pattern(/^(.*?) line (\d+) > (?:eval|Function)/),
194
+ pattern(/^(.*?):(\d+)(?::\d+)?$/),
194
195
  ];
195
196
  function locate(fn, location) {
196
197
  for (const shape of LOCATIONS) {
@@ -4,24 +4,26 @@
4
4
  import { Segment } from "../core/segment.js";
5
5
  import { Style } from "../core/style.js";
6
6
  import { RichText } from "../core/text.js";
7
- import type { Composite, Renderable, Measurable, RenderOptions } from "../core/protocol.js";
7
+ import type { Renderable, Measurable, RenderOptions } from "../core/protocol.js";
8
8
  export interface TreeOptions {
9
9
  expanded?: boolean;
10
10
  hideRoot?: boolean;
11
11
  guide_style?: string | Style;
12
12
  style?: string | Style;
13
13
  }
14
- export declare class Tree implements Renderable, Measurable, Composite {
14
+ export declare class Tree implements Renderable, Measurable {
15
15
  readonly label: Renderable;
16
- readonly children: Tree[];
17
- expanded: boolean;
16
+ private readonly _children;
17
+ private readonly _expanded;
18
18
  readonly hideRoot: boolean;
19
19
  readonly guideStyle: string | Style;
20
20
  readonly style: string | Style;
21
21
  constructor(label: string | RichText | Renderable, options?: TreeOptions);
22
22
  add(label: string | RichText | Renderable, options?: TreeOptions): Tree;
23
+ get children(): readonly Tree[];
24
+ get expanded(): boolean;
25
+ set expanded(value: boolean);
23
26
  render(rawOptions: RenderOptions): Iterable<Segment>;
24
- contents(): Iterable<Renderable>;
25
27
  /**
26
28
  * The rows `render` and `measure` both read, entered in exactly one place so
27
29
  * the two cannot start the walk differently. A hidden root takes its own row
@@ -30,7 +32,8 @@ export declare class Tree implements Renderable, Measurable, Composite {
30
32
  */
31
33
  private _walk;
32
34
  /**
33
- * The tree walked into the rows it emits, one row per label, guides first.
35
+ * This node's subtree walked into the rows it emits, one row per label,
36
+ * guides first, each guide column counted from this node.
34
37
  *
35
38
  * [LAW:one-source-of-truth] The walk that decides which rows exist and what
36
39
  * leads them is here and only here. `render` turns each row into segments and
@@ -39,35 +42,34 @@ export declare class Tree implements Renderable, Measurable, Composite {
39
42
  * them run — which they would the moment a second walk existed.
40
43
  *
41
44
  * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
42
- * this one recursion: its children get the columns above it, settled, plus a
43
- * branch of their own, so no depth is drawn by a rule of its own. A node's
44
- * style and its guide style are handed down the same way, as one more entry
45
- * on the stack its ancestors built.
45
+ * this one recursion: each child's rows get a branch column of their own,
46
+ * settled below the first of them drawn, so no depth is drawn by a rule of
47
+ * its own. A node's style and its guide style are handed down the same way,
48
+ * as one more entry on the stack its ancestors built.
49
+ *
50
+ * The branch is chosen once every child is drawn: a subtree that draws no row
51
+ * takes no place among its siblings, so the last one that draws a row ends
52
+ * the branch, and the branch stands on a subtree's first drawn row — a child
53
+ * that drew nothing above children that did leaves them on its branch, not
54
+ * the sibling's before it. Rich chooses it from the children alone, and
55
+ * draws a fork over a last child that drew nothing — an element not yet
56
+ * arrived, or gone.
46
57
  */
47
58
  private _rows;
59
+ /** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
60
+ private _subtrees;
48
61
  /**
49
- * One row of the tree: its label in whatever width the guides left, each of
50
- * the label's lines led by the guides.
51
- *
52
- * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
53
- * Before this, each call site emitted its guides and then handed the label
54
- * `options` unchanged — the label was told it had the whole outer width while
55
- * the guides had already spent four cells of it, so a tree at maxWidth 5
56
- * emitted rows of 9. That is the same defect Panel and Table were fixed for,
57
- * and the same fix: divide the requested width once and let every part read
58
- * its share off the division. `options` arrives parsed from `render`.
62
+ * One row of the tree: each of the label's lines led by the guides.
59
63
  *
60
- * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
64
+ * [LAW:single-enforcer] Tree owns its row boundaries here rather than
61
65
  * depending on label renderables to invent trailing newlines: the label's
62
66
  * output is cut into lines and every line ends where Tree ends it. A label
63
67
  * that emits nothing — one left no column, say — has no row, and one
64
68
  * that ends in a newline of its own gets the blank row that line is, as in
65
69
  * Rich — padded to the label's width when the label is justified.
66
70
  *
67
- * The node's style sits under the label's own, as Rich's `Styled` puts it, so
68
- * markup in the label still wins; its background alone reaches under the
69
- * guides, so a row with a background is filled from its first guide to the
70
- * end of its label.
71
+ * The node's background alone reaches under the guides, so a row with a
72
+ * background is filled from its first guide to the end of its label.
71
73
  */
72
74
  private _renderRow;
73
75
  measure(options: RenderOptions): {
@@ -7,6 +7,7 @@ import { Style, NULL_STYLE } from "../core/style.js";
7
7
  import { embed } from "./embed.js";
8
8
  import { drawable, getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
9
  import { Measurement } from "../core/measure.js";
10
+ import { ListState, State } from "../core/state.js";
10
11
  /** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
11
12
  const GUIDE_CELLS = 4;
12
13
  const LIGHT_GUIDES = { fork: "├── ", end: "└── ", continue: "│ ", space: " " };
@@ -28,21 +29,38 @@ function glyphsFor(options, style) {
28
29
  const weight = style.bold === true ? HEAVY_GUIDES : style.underline2 === true ? DOUBLE_GUIDES : LIGHT_GUIDES;
29
30
  return drawable(options, weight, ASCII_GUIDES, guideGlyphs);
30
31
  }
32
+ /** A row a label is known to draw, unread. */
33
+ const A_ROW = [[]];
34
+ /** The cells a row with `columns` of guide leaves its label. */
35
+ const labelRoom = (options, columns) => Math.max(0, options.maxWidth - columns * GUIDE_CELLS);
36
+ /**
37
+ * `label` drawn in `width` cells, as both `render` and `measure` draw it, so
38
+ * whether it draws a row is answered once. Cropped, so a label that ignores the
39
+ * width it is handed cannot push the row past the offer the guides were fitted
40
+ * into. No highlighter: a label is drawn plain, as Rich's `Tree` hands its
41
+ * labels `highlight=False`.
42
+ */
43
+ function drawLabel(options, label, width) {
44
+ const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
45
+ return Segment.cropLines(render(label, labelOptions), width);
46
+ }
31
47
  /** How wide a label wants to be. */
32
48
  function labelWidth(options, label) {
33
49
  return Measurement.get(options, label).maximum;
34
50
  }
51
+ /** Whether a row is drawn: a subtree with one takes a place among its siblings. */
52
+ const isDrawn = (row) => row.lines.length > 0;
35
53
  export class Tree {
36
54
  label;
37
- children;
38
- expanded;
55
+ // Rich's Tree grows by `add` and is expanded or collapsed after it is built.
56
+ _children = new ListState();
57
+ _expanded;
39
58
  hideRoot;
40
59
  guideStyle;
41
60
  style;
42
61
  constructor(label, options) {
43
62
  this.label = embed(label);
44
- this.children = [];
45
- this.expanded = options?.expanded !== false;
63
+ this._expanded = new State(options?.expanded !== false);
46
64
  this.hideRoot = options?.hideRoot ?? false;
47
65
  this.guideStyle = options?.guide_style ?? NULL_STYLE;
48
66
  this.style = options?.style ?? NULL_STYLE;
@@ -53,33 +71,51 @@ export class Tree {
53
71
  style: options?.style,
54
72
  expanded: options?.expanded,
55
73
  });
56
- this.children.push(child);
74
+ this._children.push(child);
57
75
  return child;
58
76
  }
77
+ get children() {
78
+ return this._children.get();
79
+ }
80
+ get expanded() {
81
+ return this._expanded.get();
82
+ }
83
+ set expanded(value) {
84
+ this._expanded.set(value);
85
+ }
59
86
  *render(rawOptions) {
60
87
  const options = withBoundedWidth(rawOptions, this);
61
- for (const row of this._walk()) {
62
- yield* this._renderRow(options, row);
88
+ const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
89
+ // [LAW:single-enforcer] The row's width is divided in exactly one place,
90
+ // here. Before this, each call site emitted its guides and then handed the
91
+ // label `options` unchanged — the label was told it had the whole outer
92
+ // width while the guides had already spent four cells of it, so a tree at
93
+ // maxWidth 5 emitted rows of 9. That is the same defect Panel and Table
94
+ // were fixed for, and the same fix: divide the requested width once and
95
+ // let every part read its share off the division.
96
+ //
97
+ // The guides are cropped by `cellFit`, the label by `drawLabel`. The
98
+ // node's style sits under the label's own, as Rich's `Styled` puts it, so
99
+ // markup in the label still wins.
100
+ const draw = (label, styles, columns) => Segment.splitLines(Segment.applyStyle(drawLabel(options, label, labelRoom(options, columns)), resolve(styles)));
101
+ for (const row of this._walk(draw)) {
102
+ yield* this._renderRow(options, resolve, row);
63
103
  }
64
104
  }
65
- // Every label `render` draws, from the same walk. Not `children`, which is
66
- // the subtrees, as Rich names them.
67
- *contents() {
68
- for (const row of this._walk())
69
- yield row.label;
70
- }
71
105
  /**
72
106
  * The rows `render` and `measure` both read, entered in exactly one place so
73
107
  * the two cannot start the walk differently. A hidden root takes its own row
74
108
  * and the column its children hang from with it, as Rich does — its children
75
109
  * stand at the left edge, and its guide style still reaches their guides.
76
110
  */
77
- _walk() {
78
- const rows = [...this._rows([], [], [])];
79
- return this.hideRoot ? rows.slice(1).map((row) => ({ ...row, guides: row.guides.slice(1) })) : rows;
111
+ _walk(draw) {
112
+ return this.hideRoot
113
+ ? this._subtrees(0, draw, [this.guideStyle], [this.style]).flat()
114
+ : this._rows(0, draw, [], []);
80
115
  }
81
116
  /**
82
- * The tree walked into the rows it emits, one row per label, guides first.
117
+ * This node's subtree walked into the rows it emits, one row per label,
118
+ * guides first, each guide column counted from this node.
83
119
  *
84
120
  * [LAW:one-source-of-truth] The walk that decides which rows exist and what
85
121
  * leads them is here and only here. `render` turns each row into segments and
@@ -88,50 +124,54 @@ export class Tree {
88
124
  * them run — which they would the moment a second walk existed.
89
125
  *
90
126
  * [LAW:dataflow-not-control-flow] Every node, at every depth, is walked by
91
- * this one recursion: its children get the columns above it, settled, plus a
92
- * branch of their own, so no depth is drawn by a rule of its own. A node's
93
- * style and its guide style are handed down the same way, as one more entry
94
- * on the stack its ancestors built.
127
+ * this one recursion: each child's rows get a branch column of their own,
128
+ * settled below the first of them drawn, so no depth is drawn by a rule of
129
+ * its own. A node's style and its guide style are handed down the same way,
130
+ * as one more entry on the stack its ancestors built.
131
+ *
132
+ * The branch is chosen once every child is drawn: a subtree that draws no row
133
+ * takes no place among its siblings, so the last one that draws a row ends
134
+ * the branch, and the branch stands on a subtree's first drawn row — a child
135
+ * that drew nothing above children that did leaves them on its branch, not
136
+ * the sibling's before it. Rich chooses it from the children alone, and
137
+ * draws a fork over a last child that drew nothing — an element not yet
138
+ * arrived, or gone.
95
139
  */
96
- *_rows(guides, inheritedGuideStyles, inheritedStyles) {
140
+ _rows(columns, draw, inheritedGuideStyles, inheritedStyles) {
97
141
  const styles = [...inheritedStyles, this.style];
98
- yield { guides, label: this.label, styles };
99
- const children = this.expanded ? this.children : [];
100
142
  const guideStyles = [...inheritedGuideStyles, this.guideStyle];
101
- const above = guides.map((guide) => ({ ...guide, first: guide.rest }));
102
- for (let i = 0; i < children.length; i++) {
103
- const branch = i === children.length - 1
104
- ? { first: "end", rest: "space", styles: guideStyles }
105
- : { first: "fork", rest: "continue", styles: guideStyles };
106
- yield* children[i]._rows([...above, branch], guideStyles, styles);
107
- }
143
+ const own = { guides: [], label: this.label, styles, lines: draw(this.label, styles, columns) };
144
+ const subtrees = this._subtrees(columns + 1, draw, guideStyles, styles);
145
+ const firsts = subtrees.map((rows) => rows.findIndex(isDrawn));
146
+ const last = firsts.reduce((found, first, i) => (first >= 0 ? i : found), -1);
147
+ const branched = subtrees.flatMap((rows, i) => {
148
+ const branch = i < last
149
+ ? { first: "fork", rest: "continue", styles: guideStyles }
150
+ : { first: "end", rest: "space", styles: guideStyles };
151
+ const settled = { ...branch, first: branch.rest };
152
+ return rows.map((row, r) => ({ ...row, guides: [r === firsts[i] ? branch : settled, ...row.guides] }));
153
+ });
154
+ return [own, ...branched];
155
+ }
156
+ /** Each of this node's children walked into its rows, `columns` of guide before each child's own. */
157
+ _subtrees(columns, draw, guideStyles, styles) {
158
+ const children = this.expanded ? this.children : [];
159
+ return children.map((child) => child._rows(columns, draw, guideStyles, styles));
108
160
  }
109
161
  /**
110
- * One row of the tree: its label in whatever width the guides left, each of
111
- * the label's lines led by the guides.
162
+ * One row of the tree: each of the label's lines led by the guides.
112
163
  *
113
- * [LAW:single-enforcer] The row's width is divided in exactly one place, here.
114
- * Before this, each call site emitted its guides and then handed the label
115
- * `options` unchanged — the label was told it had the whole outer width while
116
- * the guides had already spent four cells of it, so a tree at maxWidth 5
117
- * emitted rows of 9. That is the same defect Panel and Table were fixed for,
118
- * and the same fix: divide the requested width once and let every part read
119
- * its share off the division. `options` arrives parsed from `render`.
120
- *
121
- * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
164
+ * [LAW:single-enforcer] Tree owns its row boundaries here rather than
122
165
  * depending on label renderables to invent trailing newlines: the label's
123
166
  * output is cut into lines and every line ends where Tree ends it. A label
124
167
  * that emits nothing — one left no column, say — has no row, and one
125
168
  * that ends in a newline of its own gets the blank row that line is, as in
126
169
  * Rich — padded to the label's width when the label is justified.
127
170
  *
128
- * The node's style sits under the label's own, as Rich's `Styled` puts it, so
129
- * markup in the label still wins; its background alone reaches under the
130
- * guides, so a row with a background is filled from its first guide to the
131
- * end of its label.
171
+ * The node's background alone reaches under the guides, so a row with a
172
+ * background is filled from its first guide to the end of its label.
132
173
  */
133
- *_renderRow(options, row) {
134
- const resolve = (styles) => Style.combine(styles.map((style) => getStyle(options, style)));
174
+ *_renderRow(options, resolve, row) {
135
175
  const style = resolve(row.styles);
136
176
  const drawn = row.guides.map((guide) => {
137
177
  const guideStyle = resolve(guide.styles);
@@ -140,13 +180,7 @@ export class Tree {
140
180
  style: style.backgroundStyle.add(guideStyle).add(REMOVE_GUIDE_STYLES),
141
181
  };
142
182
  });
143
- const width = Math.max(0, options.maxWidth - row.guides.length * GUIDE_CELLS);
144
- // The guides are cropped by `cellFit`; the label is cropped too, so a label
145
- // that ignores the width it is handed cannot push the row past the offer the
146
- // guides were fitted into. No highlighter: a label is drawn plain, as
147
- // Rich's `Tree` hands its labels `highlight=False`.
148
- const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
149
- const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(render(row.label, labelOptions), width), style));
183
+ const lines = row.lines;
150
184
  for (let i = 0; i < lines.length; i++) {
151
185
  let left = options.maxWidth;
152
186
  for (let g = 0; g < row.guides.length; g++) {
@@ -168,10 +202,22 @@ export class Tree {
168
202
  // The widest row the walk emits, not the offer. Reported as the offer, a
169
203
  // tree told any parent that asked "I want all of it" — so `Panel` in fit
170
204
  // mode drew a 40-cell frame around nine cells of tree, and an unbounded
171
- // offer came back unbounded.
205
+ // offer came back unbounded. A row the walk does not draw wants nothing.
206
+ // A label that wants cells draws them, so only one that wants none can
207
+ // draw no row, and only that one is drawn to ask — in the one column it
208
+ // wants, as its row does, because under one column nothing draws. A label
209
+ // of no width that does draw is a blank row, as wide as its guides and that
210
+ // column: the width the tree, drawn at what this reports, hands it.
211
+ const widths = new Map();
212
+ const widthOf = (label) => {
213
+ const width = widths.get(label) ?? labelWidth(parsed, label);
214
+ widths.set(label, width);
215
+ return width;
216
+ };
217
+ const draw = (label, _styles, columns) => widthOf(label) > 0 ? A_ROW : Segment.splitLines(drawLabel(parsed, label, Math.min(labelRoom(parsed, columns), 1)));
172
218
  let natural = 0;
173
- for (const row of this._walk()) {
174
- natural = Math.max(natural, row.guides.length * GUIDE_CELLS + labelWidth(parsed, row.label));
219
+ for (const row of this._walk(draw).filter(isDrawn)) {
220
+ natural = Math.max(natural, row.guides.length * GUIDE_CELLS + Math.max(1, widthOf(row.label)));
175
221
  }
176
222
  const maximum = Math.min(natural, ceiling);
177
223
  // A row is its guides plus at least one cell of label — but never more than