@promptctl/rich-js 0.17.0 → 0.18.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 (147) hide show
  1. package/README.md +13 -26
  2. package/dist/core/anchor.d.ts +45 -0
  3. package/dist/core/anchor.js +39 -0
  4. package/dist/core/ansi.d.ts +65 -0
  5. package/dist/core/ansi.js +231 -0
  6. package/dist/core/box.d.ts +0 -1
  7. package/dist/core/box.js +6 -18
  8. package/dist/core/cells.d.ts +16 -5
  9. package/dist/core/cells.js +31 -10
  10. package/dist/core/color.d.ts +4 -8
  11. package/dist/core/color.js +28 -19
  12. package/dist/core/console.d.ts +13 -4
  13. package/dist/core/console.js +91 -36
  14. package/dist/core/export-html.d.ts +40 -5
  15. package/dist/core/export-html.js +111 -17
  16. package/dist/core/markup.d.ts +27 -5
  17. package/dist/core/markup.js +282 -170
  18. package/dist/core/measure.d.ts +1 -1
  19. package/dist/core/measure.js +2 -1
  20. package/dist/core/place.d.ts +30 -0
  21. package/dist/core/place.js +51 -0
  22. package/dist/core/pretty.d.ts +3 -0
  23. package/dist/core/pretty.js +5 -1
  24. package/dist/core/protocol.d.ts +136 -6
  25. package/dist/core/protocol.js +64 -2
  26. package/dist/core/render.d.ts +2 -2
  27. package/dist/core/render.js +2 -2
  28. package/dist/core/segment.d.ts +23 -4
  29. package/dist/core/segment.js +75 -16
  30. package/dist/core/spinnerData.d.ts +2 -0
  31. package/dist/core/spinnerData.js +3 -1
  32. package/dist/core/strip.d.ts +3 -7
  33. package/dist/core/strip.js +32 -35
  34. package/dist/core/style.d.ts +25 -0
  35. package/dist/core/style.js +61 -89
  36. package/dist/core/text.d.ts +16 -15
  37. package/dist/core/text.js +62 -76
  38. package/dist/host/app.d.ts +121 -0
  39. package/dist/host/app.js +255 -0
  40. package/dist/host/host-environment.d.ts +26 -0
  41. package/dist/host/host-environment.js +44 -0
  42. package/dist/host/index.d.ts +4 -2
  43. package/dist/host/index.js +2 -1
  44. package/dist/host/terminal-host.d.ts +43 -7
  45. package/dist/host/terminal-host.js +57 -3
  46. package/dist/index.d.ts +7 -3
  47. package/dist/index.js +4 -1
  48. package/dist/node/terminal-host.d.ts +31 -2
  49. package/dist/node/terminal-host.js +69 -2
  50. package/dist/node/traceback.d.ts +16 -6
  51. package/dist/node/traceback.js +30 -8
  52. package/dist/renderables/align.d.ts +2 -2
  53. package/dist/renderables/align.js +6 -19
  54. package/dist/renderables/columns.js +4 -20
  55. package/dist/renderables/embed.d.ts +50 -0
  56. package/dist/renderables/embed.js +71 -0
  57. package/dist/renderables/group.js +3 -1
  58. package/dist/renderables/layout.js +31 -34
  59. package/dist/renderables/live.d.ts +6 -3
  60. package/dist/renderables/live.js +89 -45
  61. package/dist/renderables/markdown.d.ts +1 -1
  62. package/dist/renderables/markdown.js +6 -4
  63. package/dist/renderables/padding.js +3 -2
  64. package/dist/renderables/panel.js +26 -41
  65. package/dist/renderables/progress.js +5 -8
  66. package/dist/renderables/progressBar.js +6 -5
  67. package/dist/renderables/rule.d.ts +3 -2
  68. package/dist/renderables/rule.js +9 -9
  69. package/dist/renderables/spinner.d.ts +5 -3
  70. package/dist/renderables/spinner.js +14 -8
  71. package/dist/renderables/syntax.d.ts +1 -1
  72. package/dist/renderables/syntax.js +2 -1
  73. package/dist/renderables/table.d.ts +26 -14
  74. package/dist/renderables/table.js +239 -141
  75. package/dist/renderables/traceback.js +8 -9
  76. package/dist/renderables/tree.d.ts +17 -3
  77. package/dist/renderables/tree.js +62 -71
  78. package/dist/renderables/viewport.d.ts +114 -0
  79. package/dist/renderables/viewport.js +198 -0
  80. package/dist/template-bindings/color-funcs.js +11 -13
  81. package/dist/template-bindings/index.js +4 -2
  82. package/dist/template-bindings/palette-funcs.js +6 -16
  83. package/dist/template-bindings/style-funcs.js +4 -0
  84. package/dist/themes/data/ansi-tables.d.ts +14 -0
  85. package/dist/themes/data/ansi-tables.js +49 -0
  86. package/dist/themes/data/atom-one-dark.js +19 -0
  87. package/dist/themes/data/atom-one-light.js +19 -0
  88. package/dist/themes/data/catppuccin-frappe.js +19 -0
  89. package/dist/themes/data/catppuccin-latte.js +19 -0
  90. package/dist/themes/data/catppuccin-macchiato.js +19 -0
  91. package/dist/themes/data/catppuccin-mocha.js +19 -0
  92. package/dist/themes/data/cyberpunk.js +2 -0
  93. package/dist/themes/data/default.js +2 -0
  94. package/dist/themes/data/dracula.js +19 -0
  95. package/dist/themes/data/flexoki.js +19 -0
  96. package/dist/themes/data/gruvbox.js +19 -0
  97. package/dist/themes/data/monokai.js +19 -0
  98. package/dist/themes/data/nord.js +19 -0
  99. package/dist/themes/data/rose-pine-dawn.js +19 -0
  100. package/dist/themes/data/rose-pine-moon.js +19 -0
  101. package/dist/themes/data/rose-pine.js +19 -0
  102. package/dist/themes/data/solarized-dark.js +19 -0
  103. package/dist/themes/data/solarized-light.js +19 -0
  104. package/dist/themes/data/svg-export.js +19 -0
  105. package/dist/themes/data/textual-ansi.js +2 -0
  106. package/dist/themes/data/textual-dark.js +2 -0
  107. package/dist/themes/data/textual-light.js +2 -0
  108. package/dist/themes/data/tokyo-night.js +19 -0
  109. package/dist/themes/data/types.d.ts +16 -0
  110. package/dist/themes/data/types.js +10 -1
  111. package/dist/themes/registry.d.ts +3 -2
  112. package/dist/themes/registry.js +13 -1
  113. package/dist/themes/terminalThemes.d.ts +2 -7
  114. package/dist/themes/terminalThemes.js +4 -9
  115. package/dist/widgets/button.d.ts +1 -1
  116. package/dist/widgets/button.js +4 -2
  117. package/dist/widgets/checkbox.d.ts +2 -2
  118. package/dist/widgets/checkbox.js +5 -3
  119. package/dist/widgets/dropdown.d.ts +2 -2
  120. package/dist/widgets/dropdown.js +13 -16
  121. package/dist/widgets/event-router.d.ts +21 -28
  122. package/dist/widgets/event-router.js +80 -73
  123. package/dist/widgets/focus-manager.d.ts +25 -6
  124. package/dist/widgets/focus-manager.js +63 -65
  125. package/dist/widgets/hit.d.ts +64 -0
  126. package/dist/widgets/hit.js +113 -0
  127. package/dist/widgets/index.d.ts +5 -7
  128. package/dist/widgets/index.js +3 -4
  129. package/dist/widgets/slider.d.ts +2 -2
  130. package/dist/widgets/slider.js +7 -7
  131. package/dist/widgets/static-item.d.ts +9 -15
  132. package/dist/widgets/static-item.js +9 -15
  133. package/dist/widgets/text-input.d.ts +2 -2
  134. package/dist/widgets/text-input.js +17 -14
  135. package/dist/widgets/toggle.d.ts +1 -1
  136. package/dist/widgets/toggle.js +3 -2
  137. package/dist/widgets/types.d.ts +14 -35
  138. package/dist/widgets/types.js +0 -1
  139. package/dist/widgets/widget-app.d.ts +63 -0
  140. package/dist/widgets/widget-app.js +199 -0
  141. package/dist/widgets/widget-base.d.ts +4 -6
  142. package/dist/widgets/widget-base.js +22 -30
  143. package/package.json +12 -4
  144. package/dist/host/host-stream.d.ts +0 -30
  145. package/dist/host/host-stream.js +0 -35
  146. package/dist/widgets/screen.d.ts +0 -98
  147. package/dist/widgets/screen.js +0 -530
@@ -95,6 +95,7 @@ import { Style } from "../core/style.js";
95
95
  import { ColorSpec } from "../core/color.js";
96
96
  import { cellLen, setCellSize, splitText, cellStepFrom, cellColToCodeUnitOffset, asCellCol, asCodePoint, nextCodePoint, prevCodePoint, } from "../core/cells.js";
97
97
  import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
98
+ import { drawable } from "../core/protocol.js";
98
99
  import { WidgetBase } from "./widget-base.js";
99
100
  /**
100
101
  * Character-greedy soft wrap. Breaks at any character once `firstWidth`
@@ -124,7 +125,12 @@ export const charGreedyWrap = (line, { firstWidth, continuationWidth }) => {
124
125
  return rows;
125
126
  };
126
127
  const MIN_CONTENT_WIDTH = 8;
128
+ // Each glyph and its ASCII stand-in are one code unit and one cell, so the
129
+ // display string maps 1:1 onto `value` and hit-testing needs no render options.
127
130
  const NEWLINE_GLYPH = "↵";
131
+ const ASCII_NEWLINE_GLYPH = "~";
132
+ const MASK_GLYPH = "•";
133
+ const ASCII_MASK_GLYPH = "*";
128
134
  // Word-character regex used by Alt+B/F and Ctrl+Left/Right. Matches the
129
135
  // readline default (alphanumeric + underscore), which is what users typing
130
136
  // shell commands or code identifiers expect.
@@ -510,17 +516,13 @@ let TextInput = (() => {
510
516
  return;
511
517
  if (event.type !== "mouse_down")
512
518
  return;
513
- const b = this.bounds;
514
- if (!b)
515
- return;
516
519
  if (this._multiline) {
517
520
  // Multiline has no brackets; rows may have a leading continuation marker.
518
521
  // Use event.y to pick the visual row, then project event.x into its content.
519
522
  const rows = this._visualRows;
520
523
  if (!rows || rows.length === 0)
521
524
  return;
522
- const relY = event.y - b.y;
523
- const rawRowIdx = this._scrollStart + relY;
525
+ const rawRowIdx = this._scrollStart + event.y;
524
526
  // Clicks on minRows-padded empty rows (beyond the last real row) snap to end.
525
527
  if (rawRowIdx >= rows.length) {
526
528
  this.cursorPosition = asCodePoint(this.value.length);
@@ -531,7 +533,7 @@ let TextInput = (() => {
531
533
  const row = rows[rowIdx];
532
534
  // Continuation rows start after the marker; non-continuation rows start at column 0.
533
535
  const contentXOff = asCellCol(row.isContinuation ? this._markerWidth : 0);
534
- const relX = asCellCol(Math.max(0, event.x - b.x - contentXOff));
536
+ const relX = asCellCol(Math.max(0, event.x - contentXOff));
535
537
  const pos = cellColToCodeUnitOffset(row.content, relX);
536
538
  const absPos = row.valueStart + pos;
537
539
  const chu = absPos < this.value.length ? this.value.charCodeAt(absPos) : 0;
@@ -545,9 +547,9 @@ let TextInput = (() => {
545
547
  // display string. Add _singleLineViewportStart (the scroll offset captured
546
548
  // by _renderSingleLine) so a click on a scrolled viewport maps to the
547
549
  // correct code-unit in the underlying value.
548
- const relX = asCellCol(Math.max(0, event.x - b.x - 1) + this._singleLineViewportStart);
550
+ const relX = asCellCol(Math.max(0, event.x - 1) + this._singleLineViewportStart);
549
551
  const displayForHitTest = this._password
550
- ? "•".repeat(this.value.length)
552
+ ? MASK_GLYPH.repeat(this.value.length)
551
553
  : this.value.indexOf("\n") >= 0
552
554
  ? this.value.replace(/\n/g, NEWLINE_GLYPH)
553
555
  : this.value;
@@ -872,7 +874,7 @@ let TextInput = (() => {
872
874
  this.emitChange();
873
875
  }
874
876
  // --- Rendering ---
875
- render(options) {
877
+ draw(options) {
876
878
  if (this._multiline)
877
879
  return this._renderMultiline(options);
878
880
  return this._renderSingleLine(options);
@@ -886,9 +888,9 @@ let TextInput = (() => {
886
888
  const rawDisplay = showPlaceholder
887
889
  ? this.placeholder
888
890
  : this._password
889
- ? "•".repeat(this.value.length)
891
+ ? drawable(options, MASK_GLYPH, ASCII_MASK_GLYPH).repeat(this.value.length)
890
892
  : this.value.indexOf("\n") >= 0
891
- ? this.value.replace(/\n/g, NEWLINE_GLYPH)
893
+ ? this.value.replace(/\n/g, drawable(options, NEWLINE_GLYPH, ASCII_NEWLINE_GLYPH))
892
894
  : this.value;
893
895
  // Measure in cell space — cursorPosition is a code-unit index; convert to
894
896
  // cell column for all viewport arithmetic.
@@ -1006,14 +1008,15 @@ let TextInput = (() => {
1006
1008
  if (i > 0)
1007
1009
  segments.push(new Segment("\n"));
1008
1010
  if (row.isContinuation) {
1009
- segments.push(new Segment(this._continuationMarker, markerStyle));
1011
+ // An ASCII stand-in as wide as the marker, so the rows keep their geometry.
1012
+ segments.push(new Segment(drawable(options, this._continuationMarker, ">".padEnd(this._markerWidth)), markerStyle));
1010
1013
  }
1011
1014
  let indicator;
1012
1015
  if (i === 0 && canScrollUp) {
1013
- indicator = { ch: "▲", style: indicatorStyle };
1016
+ indicator = { ch: drawable(options, "▲", "^"), style: indicatorStyle };
1014
1017
  }
1015
1018
  else if (i === visibleCount - 1 && canScrollDown) {
1016
- indicator = { ch: "▼", style: indicatorStyle };
1019
+ indicator = { ch: drawable(options, "▼", "v"), style: indicatorStyle };
1017
1020
  }
1018
1021
  // When a continuation marker was emitted, the row's printable width is
1019
1022
  // reduced by the marker width — the indicator column is still at
@@ -35,7 +35,7 @@ export declare class Toggle extends WidgetBase {
35
35
  setTheme(theme: TerminalTheme): void;
36
36
  handleKey(event: KeyEvent): void;
37
37
  handleMouse(event: WidgetMouseEvent): void;
38
- render(_options: RenderOptions): Iterable<Segment>;
38
+ protected draw(_options: RenderOptions): Iterable<Segment>;
39
39
  measure(_options: RenderOptions): {
40
40
  minimum: number;
41
41
  maximum: number;
@@ -145,13 +145,14 @@ let Toggle = (() => {
145
145
  handleMouse(event) {
146
146
  if (this.disabled)
147
147
  return;
148
- if (event.type === "mouse_up") {
148
+ // A release that dragged off the toggle cancels the press.
149
+ if (event.type === "mouse_up" && event.over) {
149
150
  this.on = !this.on;
150
151
  this.emitChange();
151
152
  }
152
153
  }
153
154
  // --- Rendering ---
154
- render(_options) {
155
+ draw(_options) {
155
156
  // Both indicators are exactly 5 cells: "[ON] " and "[OFF]".
156
157
  const indicator = this.on ? "[ON] " : "[OFF]";
157
158
  const text = `${indicator} ${this.label}`;
@@ -30,23 +30,24 @@ export type KeyHandlerPriority = "high" | "normal";
30
30
  export interface KeyHandlerOptions {
31
31
  priority?: KeyHandlerPriority;
32
32
  }
33
- export interface WidgetMouseEvent {
34
- type: "mouse_down" | "mouse_up" | "mouse_move" | "scroll_up" | "scroll_down";
33
+ export type ScreenMouseEvent = {
34
+ [T in MouseEventType]: MouseEventOf<T>;
35
+ }[MouseEventType];
36
+ type MouseEventType = "mouse_down" | "mouse_up" | "mouse_move" | "scroll_up" | "scroll_down";
37
+ interface MouseEventOf<Type extends string> {
38
+ type: Type;
35
39
  x: number;
36
40
  y: number;
37
41
  button: number;
38
42
  shift: boolean;
39
43
  ctrl: boolean;
40
44
  }
45
+ export interface WidgetMouseEvent extends MouseEventOf<"mouse_down" | "mouse_up" | "mouse_move"> {
46
+ over: boolean;
47
+ }
41
48
  export interface WidgetFocusEvent {
42
49
  type: "focus" | "blur";
43
50
  }
44
- export interface WidgetBounds {
45
- x: number;
46
- y: number;
47
- width: number;
48
- height: number;
49
- }
50
51
  export interface InteractiveWidget extends Renderable, Measurable {
51
52
  readonly id: string;
52
53
  readonly focusable: boolean;
@@ -54,8 +55,6 @@ export interface InteractiveWidget extends Renderable, Measurable {
54
55
  hovered: boolean;
55
56
  active: boolean;
56
57
  disabled: boolean;
57
- visible: boolean;
58
- bounds: WidgetBounds | null;
59
58
  handleKey(event: KeyEvent): void;
60
59
  handleMouse(event: WidgetMouseEvent): void;
61
60
  handleFocus(event: WidgetFocusEvent): void;
@@ -63,46 +62,26 @@ export interface InteractiveWidget extends Renderable, Measurable {
63
62
  blur(): void;
64
63
  setDisabled(value: boolean): void;
65
64
  setHovered(value: boolean): void;
66
- containsPoint(x: number, y: number): boolean;
67
65
  onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
68
66
  onSubmit(handler: (widget: InteractiveWidget) => void): Unsubscribe;
69
67
  }
70
- export type Placement = {
71
- kind: "flow";
72
- } | {
73
- kind: "inline";
74
- } | {
75
- kind: "fixed";
76
- x: number;
77
- y: number;
78
- };
79
- export declare const FLOW: Placement;
80
68
  export interface OverlayRenderable {
81
69
  renderOverlay(options: RenderOptions): Iterable<Segment> | null;
82
70
  }
83
71
  export declare function hasOverlay(value: object): value is OverlayRenderable;
84
72
  export interface FocusManager {
85
73
  readonly current: InteractiveWidget | null;
74
+ /** The widgets Tab moves between, in the order it moves. */
86
75
  readonly widgets: readonly InteractiveWidget[];
87
- register(widget: InteractiveWidget): void;
88
- unregister(widget: InteractiveWidget): void;
89
76
  next(): void;
90
77
  prev(): void;
91
78
  focus(widget: InteractiveWidget): void;
92
- blur(): void;
93
79
  handleKey(event: KeyEvent): void;
94
80
  onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
95
81
  }
96
- export type MountEntry = InteractiveWidget | {
97
- widget: InteractiveWidget;
98
- placement: Placement;
99
- };
100
- export interface Screen {
101
- mount(...entries: MountEntry[]): void;
102
- unmount(widget: InteractiveWidget): void;
103
- start(): void;
104
- stop(): void;
82
+ export interface FrameSource {
105
83
  readonly focusManager: FocusManager;
106
- readonly running: boolean;
107
- readonly widgets: readonly InteractiveWidget[];
84
+ readonly frame: readonly (readonly Segment[])[];
85
+ refresh(): void;
108
86
  }
87
+ export {};
@@ -24,7 +24,6 @@ export class KeyEvent {
24
24
  // and no focused-widget handler downstream of the caller will see it.
25
25
  stop() { this._stopped = true; }
26
26
  }
27
- export const FLOW = { kind: "flow" };
28
27
  export function hasOverlay(value) {
29
28
  return ("renderOverlay" in value &&
30
29
  typeof value.renderOverlay === "function");
@@ -0,0 +1,63 @@
1
+ /**
2
+ * WidgetApp — an `App` whose view has widgets in it: keys reach the focused
3
+ * widget, the pointer reaches the widget that drew the cell under it, and a
4
+ * change to anything a frame read paints the next one.
5
+ *
6
+ * A widget goes wherever a renderable goes — in a `Panel`, a `Layout` pane, a
7
+ * `Table` cell — and nothing lays it out a second time. Everything added here
8
+ * reads the frame the view drew:
9
+ *
10
+ * - Focus moves in document order: the order widgets start to render in
11
+ * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
12
+ * Not reading order — in a `Layout` split into columns, reading order
13
+ * zigzags between the columns row by row.
14
+ * - Overlays are painted over the view's frame, in document order, directly
15
+ * below their owner's footprint (`OverlayRenderable`). Paint order is the
16
+ * z-order: a later overlay covers an earlier one.
17
+ * - A pointer event goes to the innermost widget that drew the cell under it
18
+ * (`widgetAt`), in that widget's own coordinates; the wheel goes to the
19
+ * innermost `Viewport` that can move (`scrollTargetAt`).
20
+ *
21
+ * [LAW:locality-or-seam] `App` knows nothing of widgets — `host/` depends on
22
+ * `core/` alone — and this reaches it only through the seams it already has:
23
+ * the view is a renderable, and `onPaint` hears the frame.
24
+ */
25
+ import { Segment } from "../core/segment.js";
26
+ import type { Unsubscribe } from "../core/subscription.js";
27
+ import { type AppOptions, type AppPhase } from "../host/app.js";
28
+ import type { FocusManager, FrameSource, KeyEvent, KeyHandlerOptions, ScreenMouseEvent } from "./types.js";
29
+ export declare class WidgetApp implements FrameSource {
30
+ readonly focusManager: FocusManager;
31
+ private readonly focus;
32
+ private readonly app;
33
+ private readonly router;
34
+ private readonly reaction;
35
+ private drawn;
36
+ constructor(options: AppOptions);
37
+ get phase(): AppPhase;
38
+ /** The rows on screen now, as painted: what a pointer event lands on. */
39
+ get frame(): readonly (readonly Segment[])[];
40
+ /**
41
+ * Take the terminal, paint the first frame, and route input to its widgets
42
+ * until the app stops — `App.run`, with the input.
43
+ */
44
+ run(): Promise<void>;
45
+ stop(): void;
46
+ fail(error: unknown): void;
47
+ /**
48
+ * Paint the next frame, as `App.refresh` does. A change to an observable
49
+ * the view read needs none; one no observable carries — a `Viewport`
50
+ * scrolled from a key handler — does.
51
+ */
52
+ refresh(): void;
53
+ suspend(): Promise<void>;
54
+ /**
55
+ * Hear every key. A `high` handler hears it before the focused widget, a
56
+ * `normal` one after it — and after Tab has moved focus — and a handler
57
+ * that calls `event.stop()` ends the key there.
58
+ */
59
+ onKey(handler: (event: KeyEvent) => void, options?: KeyHandlerOptions): Unsubscribe;
60
+ /** Hear every pointer event, in terminal cells, before any widget does. */
61
+ onMouse(handler: (event: ScreenMouseEvent) => void): Unsubscribe;
62
+ private compose;
63
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * WidgetApp — an `App` whose view has widgets in it: keys reach the focused
3
+ * widget, the pointer reaches the widget that drew the cell under it, and a
4
+ * change to anything a frame read paints the next one.
5
+ *
6
+ * A widget goes wherever a renderable goes — in a `Panel`, a `Layout` pane, a
7
+ * `Table` cell — and nothing lays it out a second time. Everything added here
8
+ * reads the frame the view drew:
9
+ *
10
+ * - Focus moves in document order: the order widgets start to render in
11
+ * (`RenderOptions.onDraw`), among those with a cell on the frame on screen.
12
+ * Not reading order — in a `Layout` split into columns, reading order
13
+ * zigzags between the columns row by row.
14
+ * - Overlays are painted over the view's frame, in document order, directly
15
+ * below their owner's footprint (`OverlayRenderable`). Paint order is the
16
+ * z-order: a later overlay covers an earlier one.
17
+ * - A pointer event goes to the innermost widget that drew the cell under it
18
+ * (`widgetAt`), in that widget's own coordinates; the wheel goes to the
19
+ * innermost `Viewport` that can move (`scrollTargetAt`).
20
+ *
21
+ * [LAW:locality-or-seam] `App` knows nothing of widgets — `host/` depends on
22
+ * `core/` alone — and this reaches it only through the seams it already has:
23
+ * the view is a renderable, and `onPaint` hears the frame.
24
+ */
25
+ import { Reaction } from "mobx";
26
+ import { Segment } from "../core/segment.js";
27
+ import { App } from "../host/app.js";
28
+ import { DefaultFocusManager } from "./focus-manager.js";
29
+ import { EventRouter } from "./event-router.js";
30
+ import { footprintOf, ownersOn } from "./hit.js";
31
+ import { hasOverlay } from "./types.js";
32
+ import { WidgetBase } from "./widget-base.js";
33
+ export class WidgetApp {
34
+ focusManager;
35
+ focus;
36
+ app;
37
+ router;
38
+ // Paints the next frame when anything the last one read changes.
39
+ reaction;
40
+ // The widgets the view drew for the frame on screen, in document order.
41
+ drawn = [];
42
+ constructor(options) {
43
+ const { view } = options;
44
+ this.reaction = new Reaction("WidgetApp", () => this.app.refresh());
45
+ const root = { render: (renderOptions) => this.compose(view, renderOptions) };
46
+ this.app = new App({ ...options, view: () => root });
47
+ this.focus = new DefaultFocusManager(() => {
48
+ const owners = ownersOn(this.app.frame);
49
+ return this.drawn.filter((widget) => owners.has(widget));
50
+ });
51
+ this.focusManager = this.focus;
52
+ this.router = new EventRouter({ source: this, host: options.host });
53
+ this.app.onPaint(() => {
54
+ this.focus.settle(this.drawn);
55
+ this.router.framePainted();
56
+ });
57
+ // [LAW:single-enforcer] `settle` judges every focus move, so every move
58
+ // paints — including one onto a widget no frame has read yet.
59
+ this.focus.onChange(() => this.app.refresh());
60
+ }
61
+ get phase() {
62
+ return this.app.phase;
63
+ }
64
+ /** The rows on screen now, as painted: what a pointer event lands on. */
65
+ get frame() {
66
+ return this.app.frame;
67
+ }
68
+ /**
69
+ * Take the terminal, paint the first frame, and route input to its widgets
70
+ * until the app stops — `App.run`, with the input.
71
+ */
72
+ async run() {
73
+ // App refuses a second run; this one must not end the input of the first.
74
+ if (this.app.phase !== "idle")
75
+ return this.app.run();
76
+ // Listening before the app starts, so a first frame that throws — and
77
+ // stops the host — is not followed by a subscription that restarts it.
78
+ this.router.start();
79
+ const running = this.app.run();
80
+ try {
81
+ await running;
82
+ }
83
+ finally {
84
+ this.router.stop();
85
+ this.reaction.dispose();
86
+ }
87
+ }
88
+ stop() {
89
+ this.app.stop();
90
+ }
91
+ fail(error) {
92
+ this.app.fail(error);
93
+ }
94
+ /**
95
+ * Paint the next frame, as `App.refresh` does. A change to an observable
96
+ * the view read needs none; one no observable carries — a `Viewport`
97
+ * scrolled from a key handler — does.
98
+ */
99
+ refresh() {
100
+ this.app.refresh();
101
+ }
102
+ suspend() {
103
+ return this.app.suspend();
104
+ }
105
+ /**
106
+ * Hear every key. A `high` handler hears it before the focused widget, a
107
+ * `normal` one after it — and after Tab has moved focus — and a handler
108
+ * that calls `event.stop()` ends the key there.
109
+ */
110
+ onKey(handler, options) {
111
+ return this.router.onKey(handler, options);
112
+ }
113
+ /** Hear every pointer event, in terminal cells, before any widget does. */
114
+ onMouse(handler) {
115
+ return this.router.onMouse(handler);
116
+ }
117
+ compose(view, options) {
118
+ const drawn = new Set();
119
+ // [LAW:parse-dont-validate] An owner is any object; only a `WidgetBase`
120
+ // reports itself as a widget, so this is where `object` becomes one.
121
+ const onDraw = (owner) => {
122
+ if (owner instanceof WidgetBase)
123
+ drawn.add(owner);
124
+ };
125
+ // The view, its render and the overlays are all tracked, so an observable
126
+ // any of them reads — a label, `focused`, the state that picks what the
127
+ // view shows — paints the next frame when it changes.
128
+ // An overlay reports what it draws too: a widget in it is on the frame,
129
+ // and visiting `drawn` in insertion order paints its overlay in turn.
130
+ const render = { ...options, onDraw };
131
+ const lines = tracked(this.reaction, () => {
132
+ const base = Segment.splitLines(view().render(render));
133
+ for (const widget of drawn)
134
+ paintOverlay(base, widget, render);
135
+ return base;
136
+ });
137
+ this.drawn = [...drawn];
138
+ return lines.flatMap((line) => [...line, Segment.line()]);
139
+ }
140
+ }
141
+ /**
142
+ * `fn`, with every observable it reads subscribed to by `reaction`.
143
+ *
144
+ * [LAW:no-silent-failure] mobx reports an error thrown inside `track` and
145
+ * carries on, which would paint an empty frame for a view that threw. The
146
+ * error is carried out instead, and fails the app as `App` fails on any frame
147
+ * that throws.
148
+ */
149
+ function tracked(reaction, fn) {
150
+ // Written inside `track`, which the compiler cannot see run.
151
+ let outcome = {
152
+ ok: false,
153
+ error: new Error("WidgetApp: a frame drawn after the app stopped"),
154
+ };
155
+ reaction.track(() => {
156
+ try {
157
+ outcome = { ok: true, value: fn() };
158
+ }
159
+ catch (error) {
160
+ outcome = { ok: false, error };
161
+ }
162
+ });
163
+ if (!outcome.ok)
164
+ throw outcome.error;
165
+ return outcome.value;
166
+ }
167
+ /**
168
+ * Paint `widget`'s overlay over `lines`, below its footprint and at the column
169
+ * of its row 0, each overlay row stamped as the widget's row below its last —
170
+ * so a click there reaches the widget, in its own coordinates. A widget whose
171
+ * row 0 is not on the frame has nowhere to hang an overlay, and paints none.
172
+ */
173
+ function paintOverlay(lines, widget, options) {
174
+ if (!hasOverlay(widget))
175
+ return;
176
+ const at = footprintOf(lines, widget);
177
+ if (!at)
178
+ return;
179
+ // An overlay floats over the frame; no region holds it.
180
+ const overlay = widget.renderOverlay({ ...options, height: undefined });
181
+ if (overlay === null)
182
+ return;
183
+ const width = Math.max(0, options.maxWidth - at.x);
184
+ const rows = Segment.anchorLines(Segment.splitLines(overlay), widget, at.rows).map((line) => Segment.adjustLineLength(line, width, undefined, false));
185
+ paintLines(lines, rows, at.x, at.y + at.rows);
186
+ }
187
+ // Paint `rows` into `lines` with its top-left cell at (x, y), growing `lines`
188
+ // as needed; each painted cell replaces the one beneath it.
189
+ function paintLines(lines, rows, x, y) {
190
+ while (lines.length < y + rows.length)
191
+ lines.push([]);
192
+ rows.forEach((source, i) => {
193
+ const row = lines[y + i];
194
+ // Padded to `x`, so a row that ends short of the overlay still reaches it.
195
+ const gap = Math.max(0, x - Segment.getLineLength(row));
196
+ const [before = [], , after = []] = Segment.divide([...row, new Segment(" ".repeat(gap))], [x, x + Segment.getLineLength(source)]);
197
+ lines[y + i] = [...before, ...source, ...after];
198
+ });
199
+ }
@@ -7,9 +7,9 @@
7
7
  * `accessor` members. Each decorator registers its own member, so there is no
8
8
  * makeObservable call and subclasses add observables the same way.
9
9
  */
10
- import type { Segment } from "../core/segment.js";
10
+ import { Segment } from "../core/segment.js";
11
11
  import type { RenderOptions } from "../core/protocol.js";
12
- import type { InteractiveWidget, KeyEvent, WidgetMouseEvent, WidgetFocusEvent, WidgetBounds } from "./types.js";
12
+ import type { InteractiveWidget, KeyEvent, WidgetMouseEvent, WidgetFocusEvent } from "./types.js";
13
13
  import type { Unsubscribe } from "../core/subscription.js";
14
14
  export declare abstract class WidgetBase implements InteractiveWidget {
15
15
  abstract readonly id: string;
@@ -18,8 +18,6 @@ export declare abstract class WidgetBase implements InteractiveWidget {
18
18
  accessor hovered: boolean;
19
19
  accessor active: boolean;
20
20
  accessor disabled: boolean;
21
- accessor visible: boolean;
22
- accessor bounds: WidgetBounds | null;
23
21
  private readonly changeHandlers;
24
22
  private readonly submitHandlers;
25
23
  abstract handleKey(event: KeyEvent): void;
@@ -29,12 +27,12 @@ export declare abstract class WidgetBase implements InteractiveWidget {
29
27
  blur(): void;
30
28
  setDisabled(value: boolean): void;
31
29
  setHovered(value: boolean): void;
32
- containsPoint(x: number, y: number): boolean;
33
30
  onChange(handler: (widget: InteractiveWidget) => void): Unsubscribe;
34
31
  onSubmit(handler: (widget: InteractiveWidget) => void): Unsubscribe;
35
32
  protected emitChange(): void;
36
33
  protected emitSubmit(): void;
37
- abstract render(options: RenderOptions): Iterable<Segment>;
34
+ render(options: RenderOptions): Iterable<Segment>;
35
+ protected abstract draw(options: RenderOptions): Iterable<Segment>;
38
36
  abstract measure(options: RenderOptions): {
39
37
  minimum: number;
40
38
  maximum: number;
@@ -42,6 +42,7 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
42
42
  done = true;
43
43
  };
44
44
  import { observable, action } from "mobx";
45
+ import { Segment } from "../core/segment.js";
45
46
  let WidgetBase = (() => {
46
47
  let _instanceExtraInitializers = [];
47
48
  let _focused_decorators;
@@ -56,9 +57,6 @@ let WidgetBase = (() => {
56
57
  let _disabled_decorators;
57
58
  let _disabled_initializers = [];
58
59
  let _disabled_extraInitializers = [];
59
- let _visible_decorators;
60
- let _visible_initializers = [];
61
- let _visible_extraInitializers = [];
62
60
  let _handleFocus_decorators;
63
61
  let _focus_decorators;
64
62
  let _blur_decorators;
@@ -71,7 +69,6 @@ let WidgetBase = (() => {
71
69
  _hovered_decorators = [observable];
72
70
  _active_decorators = [observable];
73
71
  _disabled_decorators = [observable];
74
- _visible_decorators = [observable];
75
72
  _handleFocus_decorators = [action];
76
73
  _focus_decorators = [action];
77
74
  _blur_decorators = [action];
@@ -81,7 +78,6 @@ let WidgetBase = (() => {
81
78
  __esDecorate(this, null, _hovered_decorators, { kind: "accessor", name: "hovered", static: false, private: false, access: { has: obj => "hovered" in obj, get: obj => obj.hovered, set: (obj, value) => { obj.hovered = value; } }, metadata: _metadata }, _hovered_initializers, _hovered_extraInitializers);
82
79
  __esDecorate(this, null, _active_decorators, { kind: "accessor", name: "active", static: false, private: false, access: { has: obj => "active" in obj, get: obj => obj.active, set: (obj, value) => { obj.active = value; } }, metadata: _metadata }, _active_initializers, _active_extraInitializers);
83
80
  __esDecorate(this, null, _disabled_decorators, { kind: "accessor", name: "disabled", static: false, private: false, access: { has: obj => "disabled" in obj, get: obj => obj.disabled, set: (obj, value) => { obj.disabled = value; } }, metadata: _metadata }, _disabled_initializers, _disabled_extraInitializers);
84
- __esDecorate(this, null, _visible_decorators, { kind: "accessor", name: "visible", static: false, private: false, access: { has: obj => "visible" in obj, get: obj => obj.visible, set: (obj, value) => { obj.visible = value; } }, metadata: _metadata }, _visible_initializers, _visible_extraInitializers);
85
81
  __esDecorate(this, null, _handleFocus_decorators, { kind: "method", name: "handleFocus", static: false, private: false, access: { has: obj => "handleFocus" in obj, get: obj => obj.handleFocus }, metadata: _metadata }, null, _instanceExtraInitializers);
86
82
  __esDecorate(this, null, _focus_decorators, { kind: "method", name: "focus", static: false, private: false, access: { has: obj => "focus" in obj, get: obj => obj.focus }, metadata: _metadata }, null, _instanceExtraInitializers);
87
83
  __esDecorate(this, null, _blur_decorators, { kind: "method", name: "blur", static: false, private: false, access: { has: obj => "blur" in obj, get: obj => obj.blur }, metadata: _metadata }, null, _instanceExtraInitializers);
@@ -101,20 +97,7 @@ let WidgetBase = (() => {
101
97
  #disabled_accessor_storage = (__runInitializers(this, _active_extraInitializers), __runInitializers(this, _disabled_initializers, false));
102
98
  get disabled() { return this.#disabled_accessor_storage; }
103
99
  set disabled(value) { this.#disabled_accessor_storage = value; }
104
- #visible_accessor_storage = (__runInitializers(this, _disabled_extraInitializers), __runInitializers(this, _visible_initializers, true));
105
- get visible() { return this.#visible_accessor_storage; }
106
- set visible(value) { this.#visible_accessor_storage = value; }
107
- #bounds_accessor_storage = (__runInitializers(this, _visible_extraInitializers), null);
108
- // [LAW:types-are-the-program] Layout output, not reactive state. In
109
- // DefaultScreen usage the sole writer is Screen.draw(); custom hosts
110
- // (test harnesses, alternative layout loops) also write directly. The
111
- // only readers are imperative input handlers (containsPoint,
112
- // handleMouse). Modeling as observable adds reactive surface no
113
- // consumer uses and creates a strict-mode hazard when draw() runs from
114
- // a microtask outside its origin autorun.
115
- get bounds() { return this.#bounds_accessor_storage; }
116
- set bounds(value) { this.#bounds_accessor_storage = value; }
117
- changeHandlers = new Set();
100
+ changeHandlers = (__runInitializers(this, _disabled_extraInitializers), new Set());
118
101
  submitHandlers = new Set();
119
102
  handleMouse(_event) { }
120
103
  handleFocus(event) {
@@ -135,20 +118,12 @@ let WidgetBase = (() => {
135
118
  this.disabled = value;
136
119
  }
137
120
  // [LAW:single-enforcer] One canonical setter for hover state lives on
138
- // the base. EventRouter calls it on every widget whose hit-test result
139
- // diverges from `hovered`; widgets that need to react to hover transitions
140
- // override handleMouse and read the synthesized `mouse_move`. The router
141
- // no longer maintains a "does this widget have setHovered" fallback path.
121
+ // the base. EventRouter calls it when the pointer moves onto or off this
122
+ // widget; widgets that need to react to hover transitions override
123
+ // handleMouse and read the synthesized `mouse_move`.
142
124
  setHovered(value) {
143
125
  this.hovered = value;
144
126
  }
145
- // --- Hit-testing ---
146
- containsPoint(x, y) {
147
- const b = this.bounds;
148
- if (!b)
149
- return false;
150
- return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height;
151
- }
152
127
  // --- Subscriptions ---
153
128
  onChange(handler) {
154
129
  this.changeHandlers.add(handler);
@@ -168,6 +143,23 @@ let WidgetBase = (() => {
168
143
  handler(this);
169
144
  }
170
145
  }
146
+ // --- Renderable + Measurable ---
147
+ // [LAW:single-enforcer] The one place a widget's cells get their anchor, so
148
+ // a hit on any cell names this widget and the cell's place in its output
149
+ // (`widgetAt`). Subclasses draw; none stamps itself. A `RichText` admits no
150
+ // anchor, so stamping has to follow every text layout `draw` does.
151
+ // Every row is padded to the widest, so the widget owns its whole rectangle
152
+ // and a click past the end of a short row still reaches it; every row ends
153
+ // in a newline, so a blank last row survives the next `splitLines`.
154
+ // Reporting itself before it draws is what puts a widget ahead of the
155
+ // widgets nested in it in document order — the order focus traverses.
156
+ render(options) {
157
+ options.onDraw?.(this);
158
+ const drawn = Segment.splitLines(this.draw(options));
159
+ const [width] = Segment.getShape(drawn);
160
+ const lines = Segment.anchorLines(drawn.map((line) => Segment.adjustLineLength(line, width)), this);
161
+ return lines.flatMap((line) => [...line, Segment.line()]);
162
+ }
171
163
  };
172
164
  })();
173
165
  export { WidgetBase };