@promptctl/rich-js 0.24.0 → 0.24.1

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 (95) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/console.js +5 -5
  9. package/dist/core/easing.d.ts +110 -10
  10. package/dist/core/easing.js +269 -36
  11. package/dist/core/json.js +2 -1
  12. package/dist/core/measure.js +2 -2
  13. package/dist/core/place.js +2 -1
  14. package/dist/core/pretty.js +2 -2
  15. package/dist/core/protocol.d.ts +73 -19
  16. package/dist/core/protocol.js +56 -15
  17. package/dist/core/render.js +2 -1
  18. package/dist/core/segment.d.ts +18 -1
  19. package/dist/core/segment.js +45 -44
  20. package/dist/core/strip.d.ts +3 -2
  21. package/dist/core/strip.js +11 -7
  22. package/dist/core/style.js +17 -6
  23. package/dist/core/text.js +22 -14
  24. package/dist/core/timeline.d.ts +84 -0
  25. package/dist/core/timeline.js +144 -0
  26. package/dist/core/tween.d.ts +49 -0
  27. package/dist/core/tween.js +69 -0
  28. package/dist/host/app.js +2 -2
  29. package/dist/index.d.ts +10 -7
  30. package/dist/index.js +13 -4
  31. package/dist/renderables/align.d.ts +3 -2
  32. package/dist/renderables/align.js +3 -0
  33. package/dist/renderables/columns.d.ts +3 -2
  34. package/dist/renderables/columns.js +7 -2
  35. package/dist/renderables/constrain.d.ts +3 -2
  36. package/dist/renderables/constrain.js +5 -1
  37. package/dist/renderables/effect.d.ts +43 -58
  38. package/dist/renderables/effect.js +58 -64
  39. package/dist/renderables/effects.d.ts +16 -14
  40. package/dist/renderables/effects.js +64 -45
  41. package/dist/renderables/embed.js +3 -2
  42. package/dist/renderables/flexStrip.d.ts +3 -2
  43. package/dist/renderables/flexStrip.js +6 -1
  44. package/dist/renderables/frames.d.ts +40 -19
  45. package/dist/renderables/frames.js +48 -21
  46. package/dist/renderables/group.d.ts +3 -2
  47. package/dist/renderables/group.js +5 -2
  48. package/dist/renderables/html-entities.d.ts +8 -0
  49. package/dist/renderables/html-entities.js +386 -0
  50. package/dist/renderables/layout.d.ts +3 -2
  51. package/dist/renderables/layout.js +15 -2
  52. package/dist/renderables/list-view.d.ts +3 -2
  53. package/dist/renderables/list-view.js +12 -5
  54. package/dist/renderables/live.js +2 -2
  55. package/dist/renderables/markdown-parse.d.ts +109 -0
  56. package/dist/renderables/markdown-parse.js +1421 -0
  57. package/dist/renderables/markdown-url.d.ts +19 -0
  58. package/dist/renderables/markdown-url.js +314 -0
  59. package/dist/renderables/markdown.d.ts +5 -7
  60. package/dist/renderables/markdown.js +356 -683
  61. package/dist/renderables/padding.d.ts +8 -20
  62. package/dist/renderables/padding.js +32 -50
  63. package/dist/renderables/panel.d.ts +26 -26
  64. package/dist/renderables/panel.js +67 -103
  65. package/dist/renderables/progress.d.ts +4 -2
  66. package/dist/renderables/progress.js +11 -2
  67. package/dist/renderables/progressBar.js +2 -2
  68. package/dist/renderables/python-space.d.ts +13 -0
  69. package/dist/renderables/python-space.js +24 -0
  70. package/dist/renderables/spinner.d.ts +26 -14
  71. package/dist/renderables/spinner.js +49 -37
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +19 -11
  74. package/dist/renderables/table.js +140 -156
  75. package/dist/renderables/traceback.d.ts +1 -1
  76. package/dist/renderables/traceback.js +6 -5
  77. package/dist/renderables/tree.d.ts +6 -5
  78. package/dist/renderables/tree.js +11 -7
  79. package/dist/renderables/viewport.d.ts +13 -5
  80. package/dist/renderables/viewport.js +41 -20
  81. package/dist/template-bindings/animation-funcs.js +2 -2
  82. package/dist/template-bindings/index.js +2 -1
  83. package/dist/themes/ramp.d.ts +18 -3
  84. package/dist/themes/ramp.js +23 -2
  85. package/dist/widgets/focus-manager.d.ts +21 -14
  86. package/dist/widgets/focus-manager.js +28 -29
  87. package/dist/widgets/index.d.ts +1 -1
  88. package/dist/widgets/static-item.d.ts +4 -3
  89. package/dist/widgets/static-item.js +9 -7
  90. package/dist/widgets/text-input.d.ts +28 -18
  91. package/dist/widgets/text-input.js +144 -101
  92. package/dist/widgets/widget-app.d.ts +19 -3
  93. package/dist/widgets/widget-app.js +39 -21
  94. package/dist/widgets/widget-base.js +0 -3
  95. package/package.json +2 -5
@@ -5,7 +5,7 @@ import { Segment } from "../core/segment.js";
5
5
  import { cellFit, cellLen, asCellCol } from "../core/cells.js";
6
6
  import { Style, NULL_STYLE } from "../core/style.js";
7
7
  import { embed } from "./embed.js";
8
- import { drawable, getStyle, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
8
+ import { drawable, getStyle, render, stackedHeight, withBoundedWidth, withCellWidth } from "../core/protocol.js";
9
9
  import { Measurement } from "../core/measure.js";
10
10
  /** Every glyph of every set is this wide, so a row's guides are as wide however they are drawn. */
11
11
  const GUIDE_CELLS = 4;
@@ -62,6 +62,12 @@ export class Tree {
62
62
  yield* this._renderRow(options, row);
63
63
  }
64
64
  }
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
+ }
65
71
  /**
66
72
  * The rows `render` and `measure` both read, entered in exactly one place so
67
73
  * the two cannot start the walk differently. A hidden root takes its own row
@@ -115,9 +121,9 @@ export class Tree {
115
121
  * [LAW:single-enforcer] Tree also owns its row boundaries here rather than
116
122
  * depending on label renderables to invent trailing newlines: the label's
117
123
  * output is cut into lines and every line ends where Tree ends it. A label
118
- * that emits nothing still holds its row, and one that ends in a newline of
119
- * its own gets the blank row that line is, as in Rich — padded to the label's
120
- * width when the label is justified.
124
+ * that emits nothing — one left no column, say — has no row, and one
125
+ * that ends in a newline of its own gets the blank row that line is, as in
126
+ * Rich — padded to the label's width when the label is justified.
121
127
  *
122
128
  * The node's style sits under the label's own, as Rich's `Styled` puts it, so
123
129
  * markup in the label still wins; its background alone reaches under the
@@ -140,9 +146,7 @@ export class Tree {
140
146
  // guides were fitted into. No highlighter: a label is drawn plain, as
141
147
  // Rich's `Tree` hands its labels `highlight=False`.
142
148
  const labelOptions = { ...options, highlight: false, maxWidth: width, height: stackedHeight(options.height) };
143
- const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(row.label.render(labelOptions), width), style));
144
- if (lines.length === 0)
145
- lines.push([]);
149
+ const lines = Segment.splitLines(Segment.applyStyle(Segment.cropLines(render(row.label, labelOptions), width), style));
146
150
  for (let i = 0; i < lines.length; i++) {
147
151
  let left = options.maxWidth;
148
152
  for (let g = 0; g < row.guides.length; g++) {
@@ -19,9 +19,9 @@
19
19
  * against exists before a render: the rows come from the budget the render is
20
20
  * handed, and the content's length from rendering it at the width it is handed.
21
21
  * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
- * are called. Each queues a move, and the next render resolves the queue in
23
- * call order, clamping after every move, against the rows and length that
24
- * render found. Calling an operation before the first render, or three of them
22
+ * are called. Each queues a move, and the next render that draws the content
23
+ * resolves the queue in call order, clamping after every move, against the
24
+ * rows and length that render found. Calling an operation before the first render, or three of them
25
25
  * between two renders, is the same code path as calling one.
26
26
  *
27
27
  * Every cell of its rows, gutter included, is stamped as its own
@@ -33,7 +33,7 @@
33
33
  */
34
34
  import { Segment } from "../core/segment.js";
35
35
  import type { Style } from "../core/style.js";
36
- import type { Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
36
+ import type { Composite, Measurable, Renderable, RenderOptions, Scrollable } from "../core/protocol.js";
37
37
  /** How one part of a scrollbar draws each row it covers. */
38
38
  export interface ScrollbarPart {
39
39
  readonly glyph: string;
@@ -60,7 +60,7 @@ export interface ViewportOptions {
60
60
  /** A scrollbar down the right edge. Absent, the content has the full width. */
61
61
  scrollbar?: Scrollbar;
62
62
  }
63
- export declare class Viewport implements Renderable, Measurable, Scrollable {
63
+ export declare class Viewport implements Renderable, Measurable, Scrollable, Composite {
64
64
  /**
65
65
  * What the viewport shows. Replace it to show new content from the same
66
66
  * scroll position — a view rebuilt every frame keeps one `Viewport`.
@@ -113,7 +113,15 @@ export declare class Viewport implements Renderable, Measurable, Scrollable {
113
113
  contentWidth(width: number): number;
114
114
  /** The cells the scrollbar's gutter takes beside the content. */
115
115
  get gutter(): number;
116
+ contents(): Iterable<Renderable>;
116
117
  render(rawOptions: RenderOptions): Iterable<Segment>;
118
+ /**
119
+ * What a render that drew the content found, made the viewport's: `extent`
120
+ * is the rows and length the next move is clamped against, a followed range
121
+ * that was in view is kept in view if the rows changed, and the queued moves
122
+ * resolve into the offset this returns.
123
+ */
124
+ private commit;
117
125
  /**
118
126
  * [LAW:one-source-of-truth] Where the queued moves leave the offset against
119
127
  * `extent`: what a render commits, and what `canScrollBy` asks from.
@@ -19,9 +19,9 @@
19
19
  * against exists before a render: the rows come from the budget the render is
20
20
  * handed, and the content's length from rendering it at the width it is handed.
21
21
  * So `scrollTo`, `scrollBy` and `ensureVisible` do not move anything when they
22
- * are called. Each queues a move, and the next render resolves the queue in
23
- * call order, clamping after every move, against the rows and length that
24
- * render found. Calling an operation before the first render, or three of them
22
+ * are called. Each queues a move, and the next render that draws the content
23
+ * resolves the queue in call order, clamping after every move, against the
24
+ * rows and length that render found. Calling an operation before the first render, or three of them
25
25
  * between two renders, is the same code path as calling one.
26
26
  *
27
27
  * Every cell of its rows, gutter included, is stamped as its own
@@ -35,7 +35,8 @@ import { Segment } from "../core/segment.js";
35
35
  import { Measurement } from "../core/measure.js";
36
36
  import { cellCount, cellLen } from "../core/cells.js";
37
37
  import { NULL_STYLE } from "../core/style.js";
38
- import { drawable, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
38
+ import { drawable, drawsNothing, fitHeight, getStyle, regionRows, withBoundedWidth, withCellWidth, } from "../core/protocol.js";
39
+ import { render } from "../core/protocol.js";
39
40
  /** A heavy line for the thumb on a light line for the track. */
40
41
  export const SCROLLBAR = {
41
42
  thumb: { glyph: "┃", style: "scrollbar.thumb" },
@@ -125,26 +126,27 @@ export class Viewport {
125
126
  get gutter() {
126
127
  return gutterWidth(this.scrollbar);
127
128
  }
129
+ *contents() {
130
+ yield this.content;
131
+ }
128
132
  *render(rawOptions) {
129
133
  const { height, ...options } = withBoundedWidth(rawOptions, this);
130
134
  const gutter = this.gutter;
131
135
  const contentWidth = this.contentWidth(options.maxWidth);
132
- const lines = Segment.splitLines(this.content.render({ ...options, maxWidth: contentWidth }));
133
- const extent = { rows: viewRows(height, this.rows, lines.length), lines: lines.length };
134
- const followed = this._followed;
135
- const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
136
- this._extent = extent;
137
- if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
138
- this.ensureVisible(followed.start, followed.end);
139
- }
140
- if (!Number.isNaN(followed.start))
141
- this._followed = { ...followed, rows: extent.rows };
142
- this._offset = this.resolve(extent);
143
- this._moves = [];
144
- const shown = fitHeight(lines.slice(this._offset, this._offset + extent.rows), { rows: extent.rows, exact: true });
145
- // [LAW:no-ambient-temporal-coupling] The thumb is drawn from the offset
146
- // this render just resolved, not from `offset` as it stood before it.
147
- const thumb = thumbRows(extent, this._offset);
136
+ const lines = Segment.splitLines(render(this.content, { ...options, maxWidth: contentWidth }));
137
+ // [LAW:no-ambient-temporal-coupling] Content handed no column is held, not
138
+ // drawn, so this render learns its budget but not the content's length. It
139
+ // commits nothing: the offset, the followed range and the queued moves wait
140
+ // for a render that draws, and this one draws the scrollbar against the
141
+ // length the last one found.
142
+ const held = drawsNothing({ maxWidth: contentWidth });
143
+ const length = held ? this._extent.lines : lines.length;
144
+ const extent = { rows: viewRows(height, this.rows, length), lines: length };
145
+ const offset = held ? clampOffset(this._offset, extent) : this.commit(extent);
146
+ const shown = fitHeight(lines.slice(offset, offset + extent.rows), { rows: extent.rows, exact: true });
147
+ // The thumb is drawn from the offset this render resolved, not from
148
+ // `offset` as it stood before it.
149
+ const thumb = thumbRows(extent, offset);
148
150
  // The content's cells and the gutter's drawn cells sum to the offer: a
149
151
  // gutter wider than the whole offer is drawn cropped to it.
150
152
  const drawn = Math.min(gutter, options.maxWidth);
@@ -168,6 +170,25 @@ export class Viewport {
168
170
  yield Segment.line();
169
171
  }
170
172
  }
173
+ /**
174
+ * What a render that drew the content found, made the viewport's: `extent`
175
+ * is the rows and length the next move is clamped against, a followed range
176
+ * that was in view is kept in view if the rows changed, and the queued moves
177
+ * resolve into the offset this returns.
178
+ */
179
+ commit(extent) {
180
+ const followed = this._followed;
181
+ const wasShown = followed.start >= this._offset && followed.end <= this._offset + this._extent.rows;
182
+ this._extent = extent;
183
+ if (!Number.isNaN(followed.rows) && followed.rows !== extent.rows && wasShown) {
184
+ this.ensureVisible(followed.start, followed.end);
185
+ }
186
+ if (!Number.isNaN(followed.start))
187
+ this._followed = { ...followed, rows: extent.rows };
188
+ this._offset = this.resolve(extent);
189
+ this._moves = [];
190
+ return this._offset;
191
+ }
171
192
  /**
172
193
  * [LAW:one-source-of-truth] Where the queued moves leave the offset against
173
194
  * `extent`: what a render commits, and what `canScrollBy` asks from.
@@ -27,7 +27,7 @@
27
27
  * colour — is parsed in `pairs`, once. [LAW:types-are-the-program]
28
28
  */
29
29
  import { beat } from "../core/easing.js";
30
- import { FrameSequence } from "../renderables/frames.js";
30
+ import { FrameSequence, frameAt } from "../renderables/frames.js";
31
31
  import { asColor } from "./color-funcs.js";
32
32
  import { asFragment } from "./helpers.js";
33
33
  /**
@@ -64,7 +64,7 @@ export function animationFuncs(frameTime) {
64
64
  const framesFunc = {
65
65
  fn: ((...tail) => {
66
66
  const frames = pairs("frame", tail, asFragment).map(({ seconds, value }) => ({ seconds, text: value }));
67
- return new FrameSequence(frames).text(frameTime());
67
+ return frameAt(new FrameSequence(frames), frameTime());
68
68
  }),
69
69
  argTypes: ["float", "liftable"],
70
70
  arity: { kind: "alternating", minimum: 2 },
@@ -40,6 +40,7 @@ import { createEngine } from "@promptctl/go-template-js";
40
40
  import { RichText } from "../core/text.js";
41
41
  import { Style } from "../core/style.js";
42
42
  import { Segment } from "../core/segment.js";
43
+ import { render } from "../core/protocol.js";
43
44
  import { richTextStyleFuncs } from "./style-funcs.js";
44
45
  import { colorFuncs } from "./color-funcs.js";
45
46
  export { paletteFuncs } from "./palette-funcs.js";
@@ -131,7 +132,7 @@ export function renderTemplate(engine, source, scope = {}, options) {
131
132
  catch (e) {
132
133
  return [new Segment(errorLine(e, maxWidth), safeErrorStyle(errorStyle))];
133
134
  }
134
- return Array.from(text.render({ isTerminal: true, ...renderOptions, maxWidth }));
135
+ return Array.from(render(text, { isTerminal: true, ...renderOptions, maxWidth }));
135
136
  }
136
137
  /**
137
138
  * A caught error as the one line `renderTemplate` promises. The summary is the
@@ -20,12 +20,14 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
23
- * [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
24
- * only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
23
+ * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
24
+ * and for `over`, `core/animation` and `core/tween`; nothing here knows
25
+ * what a palette is. Resolving stop *names* is the template binding's
25
26
  * job (`paletteFuncs`), which hands this module resolved colors.
26
27
  */
28
+ import { Animation } from "../core/animation.js";
27
29
  import type { ColorRgba } from "../core/color.js";
28
- import type { Ease } from "../core/easing.js";
30
+ import { type Ease } from "../core/easing.js";
29
31
  /** One stop: the color the ramp is exactly `color` at position `at`. */
30
32
  export interface ColorStop {
31
33
  readonly at: number;
@@ -67,4 +69,17 @@ export declare class ColorRamp {
67
69
  * where it does: a ramp never paints outside its stops.
68
70
  */
69
71
  at(value: number): ColorRgba;
72
+ /**
73
+ * This ramp read over time: its span of positions, from the first stop's
74
+ * to the last's, crossed at a steady rate in `seconds` and held at the last
75
+ * stop from then on, each colour along the way the ramp's own. A colour
76
+ * tween is a two-stop ramp over its seconds — the ramp's ease shaping the
77
+ * move, so `new ColorRamp(ease, [{ at: 0, color: a }, { at: 1, color: b
78
+ * }]).over(seconds)` is `a` eased to `b` — and a ramp of more stops over
79
+ * time is a sweep through all of them. [LAW:one-source-of-truth] The
80
+ * blending is `at`'s; what time adds is `tween`'s (`core/tween.ts`), so a
81
+ * host sampling once a second or sixty times reads the stop colours byte
82
+ * for byte where `at` paints them.
83
+ */
84
+ over(seconds: number): Animation<ColorRgba>;
70
85
  }
@@ -20,11 +20,15 @@
20
20
  * theme colors passes through perceptually even steps rather than the muddy
21
21
  * midpoints of an sRGB blend.
22
22
  *
23
- * [LAW:one-way-deps] Imports `core/color`, `core/easing` and `core/oklch`
24
- * only; nothing here knows what a palette is. Resolving stop *names* is the template binding's
23
+ * [LAW:one-way-deps] Imports `core/color`, `core/easing`, `core/oklch`,
24
+ * and for `over`, `core/animation` and `core/tween`; nothing here knows
25
+ * what a palette is. Resolving stop *names* is the template binding's
25
26
  * job (`paletteFuncs`), which hands this module resolved colors.
26
27
  */
28
+ import { Animation } from "../core/animation.js";
29
+ import { EASES } from "../core/easing.js";
27
30
  import { Oklch } from "../core/oklch.js";
31
+ import { tween } from "../core/tween.js";
28
32
  /**
29
33
  * An immutable ramp over stops sorted by position.
30
34
  *
@@ -100,4 +104,21 @@ export class ColorRamp {
100
104
  return to.color;
101
105
  return Oklch.fromRgba(from.color).mix(Oklch.fromRgba(to.color), t).toRgba();
102
106
  }
107
+ /**
108
+ * This ramp read over time: its span of positions, from the first stop's
109
+ * to the last's, crossed at a steady rate in `seconds` and held at the last
110
+ * stop from then on, each colour along the way the ramp's own. A colour
111
+ * tween is a two-stop ramp over its seconds — the ramp's ease shaping the
112
+ * move, so `new ColorRamp(ease, [{ at: 0, color: a }, { at: 1, color: b
113
+ * }]).over(seconds)` is `a` eased to `b` — and a ramp of more stops over
114
+ * time is a sweep through all of them. [LAW:one-source-of-truth] The
115
+ * blending is `at`'s; what time adds is `tween`'s (`core/tween.ts`), so a
116
+ * host sampling once a second or sixty times reads the stop colours byte
117
+ * for byte where `at` paints them.
118
+ */
119
+ over(seconds) {
120
+ const first = this.stops[0].at;
121
+ const last = this.stops[this.stops.length - 1].at;
122
+ return Animation.map(tween(first, last, { seconds, ease: EASES.linear }), (value) => this.at(value));
123
+ }
103
124
  }
@@ -6,18 +6,23 @@
6
6
  */
7
7
  import type { InteractiveWidget, FocusManager, KeyEvent } from "./types.js";
8
8
  import type { Unsubscribe } from "../core/subscription.js";
9
+ /** What focus follows: the frame a `WidgetApp` just painted. */
10
+ export interface FocusView {
11
+ /** Every widget the view holds, in document order, whether or not it has a cell on screen. */
12
+ present(): readonly InteractiveWidget[];
13
+ /** The widgets with a cell on the frame on screen, in document order. */
14
+ onScreen(): readonly InteractiveWidget[];
15
+ }
9
16
  export declare class DefaultFocusManager implements FocusManager {
10
- private readonly onScreen;
17
+ private readonly view;
11
18
  accessor currentWidget: InteractiveWidget | null;
12
19
  private readonly changeHandlers;
13
20
  /**
14
- * `onScreen` is the widgets on the frame on screen, in document order.
15
- *
16
- * [LAW:one-source-of-truth] It is asked every time rather than kept: a list
17
- * of the widgets held here would be a second picture of the frame, and
18
- * wrong from the next paint.
21
+ * [LAW:one-source-of-truth] The view is asked every time rather than kept:
22
+ * a list of the widgets held here would be a second picture of the frame,
23
+ * and wrong from the next paint.
19
24
  */
20
- constructor(onScreen: () => readonly InteractiveWidget[]);
25
+ constructor(view: FocusView);
21
26
  get current(): InteractiveWidget | null;
22
27
  get widgets(): readonly InteractiveWidget[];
23
28
  next(): void;
@@ -25,20 +30,22 @@ export declare class DefaultFocusManager implements FocusManager {
25
30
  focus(widget: InteractiveWidget): void;
26
31
  /**
27
32
  * Put focus where the frame just painted lets it rest: it stays on a widget
28
- * the view drew (`drawn`) that can take it, and otherwise moves to the first
29
- * one on screen that can, or to none.
33
+ * the view holds (`present`) that can take it, and otherwise moves to the
34
+ * first one on screen that can, or to none.
30
35
  *
31
- * Drawn, not on screen: a widget cropped for the moment — the terminal
32
- * shrank, a viewport scrolled, an overlay covers it — is still in the view,
33
- * and keeps focus and whatever focus holds, like a half-typed filter.
36
+ * In the view, not on screen: a widget cropped for the moment — the
37
+ * terminal shrank, a viewport scrolled, an overlay covers it, its container
38
+ * handed it no column — is still in the view, and keeps focus and whatever
39
+ * focus holds, like a half-typed filter.
34
40
  *
35
41
  * [LAW:single-enforcer] The one place focus follows the frame — onto the
36
- * first widget when the app starts, off a widget that stopped being drawn
42
+ * first widget when the app starts, off a widget that left the view
37
43
  * or was disabled.
38
44
  */
39
- settle(drawn: readonly InteractiveWidget[]): void;
45
+ settle(): void;
40
46
  onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
41
47
  handleKey(event: KeyEvent): void;
48
+ private step;
42
49
  private clear;
43
50
  private setFocus;
44
51
  private emitChange;
@@ -69,42 +69,30 @@ let DefaultFocusManager = (() => {
69
69
  __esDecorate(this, null, _setFocus_decorators, { kind: "method", name: "setFocus", static: false, private: false, access: { has: obj => "setFocus" in obj, get: obj => obj.setFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
70
70
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
71
71
  }
72
- onScreen = __runInitializers(this, _instanceExtraInitializers);
72
+ view = __runInitializers(this, _instanceExtraInitializers);
73
73
  #currentWidget_accessor_storage = __runInitializers(this, _currentWidget_initializers, null);
74
74
  get currentWidget() { return this.#currentWidget_accessor_storage; }
75
75
  set currentWidget(value) { this.#currentWidget_accessor_storage = value; }
76
76
  changeHandlers = (__runInitializers(this, _currentWidget_extraInitializers), new Set());
77
77
  /**
78
- * `onScreen` is the widgets on the frame on screen, in document order.
79
- *
80
- * [LAW:one-source-of-truth] It is asked every time rather than kept: a list
81
- * of the widgets held here would be a second picture of the frame, and
82
- * wrong from the next paint.
78
+ * [LAW:one-source-of-truth] The view is asked every time rather than kept:
79
+ * a list of the widgets held here would be a second picture of the frame,
80
+ * and wrong from the next paint.
83
81
  */
84
- constructor(onScreen) {
85
- this.onScreen = onScreen;
82
+ constructor(view) {
83
+ this.view = view;
86
84
  }
87
85
  get current() {
88
86
  return this.currentWidget;
89
87
  }
90
88
  get widgets() {
91
- return this.onScreen().filter(takesFocus);
89
+ return this.view.onScreen().filter(takesFocus);
92
90
  }
93
91
  next() {
94
- const focusable = this.widgets;
95
- if (focusable.length === 0)
96
- return;
97
- const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
98
- const nextIdx = (currentIdx + 1) % focusable.length;
99
- this.setFocus(focusable[nextIdx]);
92
+ this.step(this.view.present());
100
93
  }
101
94
  prev() {
102
- const focusable = this.widgets;
103
- if (focusable.length === 0)
104
- return;
105
- const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
106
- const prevIdx = currentIdx <= 0 ? focusable.length - 1 : currentIdx - 1;
107
- this.setFocus(focusable[prevIdx]);
95
+ this.step([...this.view.present()].reverse());
108
96
  }
109
97
  // A widget not on screen yet can take focus: an app that is about to show
110
98
  // it focuses it first, and the frame that shows it keeps it (`settle`).
@@ -115,20 +103,21 @@ let DefaultFocusManager = (() => {
115
103
  }
116
104
  /**
117
105
  * Put focus where the frame just painted lets it rest: it stays on a widget
118
- * the view drew (`drawn`) that can take it, and otherwise moves to the first
119
- * one on screen that can, or to none.
106
+ * the view holds (`present`) that can take it, and otherwise moves to the
107
+ * first one on screen that can, or to none.
120
108
  *
121
- * Drawn, not on screen: a widget cropped for the moment — the terminal
122
- * shrank, a viewport scrolled, an overlay covers it — is still in the view,
123
- * and keeps focus and whatever focus holds, like a half-typed filter.
109
+ * In the view, not on screen: a widget cropped for the moment — the
110
+ * terminal shrank, a viewport scrolled, an overlay covers it, its container
111
+ * handed it no column — is still in the view, and keeps focus and whatever
112
+ * focus holds, like a half-typed filter.
124
113
  *
125
114
  * [LAW:single-enforcer] The one place focus follows the frame — onto the
126
- * first widget when the app starts, off a widget that stopped being drawn
115
+ * first widget when the app starts, off a widget that left the view
127
116
  * or was disabled.
128
117
  */
129
- settle(drawn) {
118
+ settle() {
130
119
  const current = this.currentWidget;
131
- if (current && takesFocus(current) && drawn.includes(current))
120
+ if (current && takesFocus(current) && this.view.present().includes(current))
132
121
  return;
133
122
  const first = this.widgets[0];
134
123
  if (first)
@@ -154,6 +143,16 @@ let DefaultFocusManager = (() => {
154
143
  event.stop();
155
144
  }
156
145
  // --- Private ---
146
+ // To the first widget Tab moves between that follows the focused one in
147
+ // `order`, wrapping. It is found by its place in the view, not among those
148
+ // on screen, so Tab from a widget on no cell moves to its neighbour.
149
+ step(order) {
150
+ const tabbable = new Set(this.widgets);
151
+ const at = this.currentWidget ? order.indexOf(this.currentWidget) : -1;
152
+ const target = [...order.slice(at + 1), ...order.slice(0, at + 1)].find((widget) => tabbable.has(widget));
153
+ if (target)
154
+ this.setFocus(target);
155
+ }
157
156
  // No focus is where `settle` leaves an app with nothing to focus, and only
158
157
  // there: anywhere else the next frame would put focus back on the first
159
158
  // widget, so there is no public way in.
@@ -4,7 +4,7 @@ export { StaticItem } from "./static-item.js";
4
4
  export type { StaticItemOptions } from "./static-item.js";
5
5
  export { WidgetBase } from "./widget-base.js";
6
6
  export { WidgetApp } from "./widget-app.js";
7
- export type { WidgetAppOptions } from "./widget-app.js";
7
+ export type { WidgetAppOptions, WidgetPresence } from "./widget-app.js";
8
8
  export { Button } from "./button.js";
9
9
  export type { ButtonVariant, ButtonOptions } from "./button.js";
10
10
  export { Checkbox } from "./checkbox.js";
@@ -11,7 +11,7 @@
11
11
  * renderable.
12
12
  */
13
13
  import { Segment } from "../core/segment.js";
14
- import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
14
+ import type { Composite, Measurable, Renderable, RenderOptions } from "../core/protocol.js";
15
15
  import { WidgetBase } from "./widget-base.js";
16
16
  import type { KeyEvent } from "./types.js";
17
17
  export interface StaticItemOptions {
@@ -19,12 +19,13 @@ export interface StaticItemOptions {
19
19
  render: Renderable | ((options: RenderOptions) => Iterable<Segment>);
20
20
  measure?: Measurable["measure"];
21
21
  }
22
- export declare class StaticItem extends WidgetBase {
22
+ export declare class StaticItem extends WidgetBase implements Composite {
23
23
  readonly id: string;
24
24
  readonly focusable = false;
25
- private readonly renderFn;
25
+ private readonly body;
26
26
  private readonly measureFn;
27
27
  constructor(options: StaticItemOptions);
28
+ contents(): Iterable<Renderable>;
28
29
  handleKey(_event: KeyEvent): void;
29
30
  protected draw(options: RenderOptions): Iterable<Segment>;
30
31
  measure(options: RenderOptions): {
@@ -11,27 +11,29 @@
11
11
  * renderable.
12
12
  */
13
13
  import { Segment } from "../core/segment.js";
14
+ import { render } from "../core/protocol.js";
14
15
  import { WidgetBase } from "./widget-base.js";
15
16
  export class StaticItem extends WidgetBase {
16
17
  id;
17
18
  focusable = false;
18
- renderFn;
19
+ // The body as a renderable either way: a function is one that holds nothing.
20
+ body;
19
21
  measureFn;
20
22
  constructor(options) {
21
23
  super();
22
24
  this.id = options.id;
23
25
  const r = options.render;
24
- this.renderFn =
25
- typeof r === "function"
26
- ? (opts) => r(opts)
27
- : (opts) => r.render(opts);
26
+ this.body = typeof r === "function" ? { render: r } : r;
28
27
  this.measureFn = options.measure;
29
28
  }
29
+ *contents() {
30
+ yield this.body;
31
+ }
30
32
  // Static items don't respond to keys. WidgetBase.handleMouse / handleFocus
31
33
  // are already no-ops, so we only need to satisfy the abstract handleKey.
32
34
  handleKey(_event) { }
33
35
  draw(options) {
34
- return this.renderFn(options);
36
+ return render(this.body, options);
35
37
  }
36
38
  measure(options) {
37
39
  if (this.measureFn)
@@ -41,7 +43,7 @@ export class StaticItem extends WidgetBase {
41
43
  // values too, not just the "\n" sentinel segment. Walking segments and
42
44
  // testing `seg.text === "\n"` miscounts widths for any renderable
43
45
  // that emits "a\nb" as a single segment.
44
- const segments = Array.from(this.renderFn(options));
46
+ const segments = Array.from(render(this.body, options));
45
47
  const lines = Segment.splitLines(segments);
46
48
  let max = 0;
47
49
  for (const line of lines) {
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * Two modes:
5
5
  * - single-line (default): Enter submits; newlines in value are rendered
6
- * as a literal `↵` glyph if they ever appear.
6
+ * as a literal `↵` glyph if they ever appear, and
7
+ * any other glyph of zero cells as `◌`.
7
8
  * - multi-line (`multiline: true`): Enter inserts a `\n`; `value` may
8
9
  * contain logical line breaks; Up/Down navigate
9
10
  * between logical lines preserving column intent.
11
+ * A glyph of zero cells is drawn as `◌` here too.
10
12
  *
11
13
  * [LAW:dataflow-not-control-flow] cursor and value are observable data; the
12
14
  * keymap dispatches to small `@action` mutators that all flow through the
@@ -69,20 +71,22 @@ import { ThemedWidget } from "./themed-widget.js";
69
71
  import type { KeyEvent, WidgetMouseEvent } from "./types.js";
70
72
  /**
71
73
  * A soft-wrap strategy. Given one logical line of text and cell-width budgets,
72
- * return the visual sub-rows that line wraps into. The first sub-row uses
73
- * `firstWidth` as its capacity; every subsequent (continuation) sub-row uses
74
- * `continuationWidth` (typically `firstWidth - cellLen(continuationMarker)`).
74
+ * return the visual sub-rows that line wraps into. The line arrives as the
75
+ * widget shows it — each glyph of zero cells already its one-cell `◌`, each
76
+ * glyph of a password its mask — so the budget counts the cells drawn. The
77
+ * first sub-row uses `firstWidth` as its capacity; every subsequent
78
+ * (continuation) sub-row uses `continuationWidth` (typically
79
+ * `firstWidth - cellLen(continuationMarker)`).
75
80
  *
76
- * Each returned row carries:
77
- * - `content` — the displayed text for that visual row (the widget prepends
78
- * the continuation marker itself; the strategy never includes it).
79
- * - `start` — code-unit offset into the *logical line* where `content`
80
- * begins, so the widget can map visual positions back to absolute `value`
81
- * offsets for cursor projection.
81
+ * Each returned row is the stretch `[start, end)` of the line as given that
82
+ * the row draws; the widget draws the matching stretch of `value` and
83
+ * prepends the continuation marker itself.
82
84
  *
83
- * [LAW:types-are-the-program] Budget params are `CellCol`; `start` is
84
- * `CodePoint` (a `CodeUnit` on a code-point boundary) — the type system
85
- * prevents mixing these integer spaces inside a custom strategy.
85
+ * [LAW:types-are-the-program] A row names where it falls in the line rather
86
+ * than carrying text of its own, so it cannot draw anything the line does not
87
+ * hold. Budget params are `CellCol`; offsets are `CodePoint` (a `CodeUnit` on
88
+ * a code-point boundary) — the type system prevents mixing these integer
89
+ * spaces inside a custom strategy.
86
90
  *
87
91
  * Built-in: `charGreedyWrap` (break at any character at the width limit).
88
92
  * Custom: any function matching this signature. Template-aware wrapping
@@ -94,9 +98,10 @@ export type WrapStrategy = (logicalLine: string, budget: {
94
98
  continuationWidth: CellCol;
95
99
  }) => readonly WrapRow[];
96
100
  export interface WrapRow {
97
- readonly content: string;
98
- /** Code-point offset into the logical line where `content` begins. */
101
+ /** Offset into the line as given where the row begins. */
99
102
  readonly start: CodePoint;
103
+ /** Offset into the line as given where the row ends, exclusive. */
104
+ readonly end: CodePoint;
100
105
  }
101
106
  /**
102
107
  * Character-greedy soft wrap. Breaks at any character once `firstWidth`
@@ -225,7 +230,7 @@ export declare class TextInput extends ThemedWidget {
225
230
  private _killBuffer;
226
231
  private _scrollStart;
227
232
  private _singleLineViewportStart;
228
- private _singleLineAsciiOnly;
233
+ private _drawn;
229
234
  readonly multiline: boolean;
230
235
  constructor(options?: TextInputOptions);
231
236
  handleKey(event: KeyEvent): void;
@@ -257,12 +262,16 @@ export declare class TextInput extends ThemedWidget {
257
262
  private _insertText;
258
263
  protected get ground(): Style;
259
264
  protected draw(options: RenderOptions): Iterable<Segment>;
265
+ private _shownAs;
260
266
  private _shown;
267
+ private _shownCells;
268
+ private _offsetAtShownCell;
261
269
  private _renderSingleLine;
262
270
  private _renderMultiline;
271
+ private _drawsArrows;
263
272
  private _emitRowContent;
264
273
  private _computeVisualRows;
265
- measure(_options: RenderOptions): {
274
+ measure(options: RenderOptions): {
266
275
  minimum: number;
267
276
  maximum: number;
268
277
  };
@@ -271,7 +280,8 @@ export declare class TextInput extends ThemedWidget {
271
280
  * scroll, else `undefined`. X is the cursor's 1-indexed visual row; Y is
272
281
  * the total visual row count. Reads cached state populated by the most
273
282
  * recent `render()` — call from a `Panel.bottomRightAccessory` thunk so
274
- * it evaluates after content has been rendered for the current frame.
283
+ * it evaluates after content has been rendered for the current frame;
284
+ * `BorderAccessory` names the widths where it has not.
275
285
  *
276
286
  * Returns `undefined` when:
277
287
  * - `scrollIndicator` is not `"indices"`, or