@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * FocusManager — flat focus cycling over registered widgets.
2
+ * FocusManager — which widget has focus, over the widgets on the frame.
3
3
  * [LAW:one-source-of-truth] single authority for which widget has focus.
4
4
  * [LAW:dataflow-not-control-flow] focus transitions are observable state;
5
5
  * widgets react to focus/blur events, the manager never skips dispatch.
@@ -7,20 +7,39 @@
7
7
  import type { InteractiveWidget, FocusManager, KeyEvent } from "./types.js";
8
8
  import type { Unsubscribe } from "../core/subscription.js";
9
9
  export declare class DefaultFocusManager implements FocusManager {
10
- accessor widgetList: InteractiveWidget[];
10
+ private readonly onScreen;
11
11
  accessor currentWidget: InteractiveWidget | null;
12
12
  private readonly changeHandlers;
13
+ /**
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.
19
+ */
20
+ constructor(onScreen: () => readonly InteractiveWidget[]);
13
21
  get current(): InteractiveWidget | null;
14
22
  get widgets(): readonly InteractiveWidget[];
15
- register(widget: InteractiveWidget): void;
16
- unregister(widget: InteractiveWidget): void;
17
23
  next(): void;
18
24
  prev(): void;
19
25
  focus(widget: InteractiveWidget): void;
20
- blur(): void;
26
+ /**
27
+ * 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.
30
+ *
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.
34
+ *
35
+ * [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
37
+ * or was disabled.
38
+ */
39
+ settle(drawn: readonly InteractiveWidget[]): void;
21
40
  onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
22
41
  handleKey(event: KeyEvent): void;
23
- private focusableWidgets;
42
+ private clear;
24
43
  private setFocus;
25
44
  private emitChange;
26
45
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * FocusManager — flat focus cycling over registered widgets.
2
+ * FocusManager — which widget has focus, over the widgets on the frame.
3
3
  * [LAW:one-source-of-truth] single authority for which widget has focus.
4
4
  * [LAW:dataflow-not-control-flow] focus transitions are observable state;
5
5
  * widgets react to focus/blur events, the manager never skips dispatch.
@@ -38,89 +38,60 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
38
38
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
39
39
  done = true;
40
40
  };
41
- import { action, observableRef, observableShallow } from "mobx";
41
+ import { action, observableRef } from "mobx";
42
42
  let DefaultFocusManager = (() => {
43
43
  let _instanceExtraInitializers = [];
44
- let _widgetList_decorators;
45
- let _widgetList_initializers = [];
46
- let _widgetList_extraInitializers = [];
47
44
  let _currentWidget_decorators;
48
45
  let _currentWidget_initializers = [];
49
46
  let _currentWidget_extraInitializers = [];
50
- let _register_decorators;
51
- let _unregister_decorators;
52
47
  let _next_decorators;
53
48
  let _prev_decorators;
54
49
  let _focus_decorators;
55
- let _blur_decorators;
50
+ let _settle_decorators;
51
+ let _clear_decorators;
56
52
  let _setFocus_decorators;
57
53
  return class DefaultFocusManager {
58
54
  static {
59
55
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
60
- _widgetList_decorators = [observableShallow];
61
56
  _currentWidget_decorators = [observableRef];
62
- _register_decorators = [action];
63
- _unregister_decorators = [action];
64
57
  _next_decorators = [action];
65
58
  _prev_decorators = [action];
66
59
  _focus_decorators = [action];
67
- _blur_decorators = [action];
60
+ _settle_decorators = [action];
61
+ _clear_decorators = [action];
68
62
  _setFocus_decorators = [action];
69
- __esDecorate(this, null, _widgetList_decorators, { kind: "accessor", name: "widgetList", static: false, private: false, access: { has: obj => "widgetList" in obj, get: obj => obj.widgetList, set: (obj, value) => { obj.widgetList = value; } }, metadata: _metadata }, _widgetList_initializers, _widgetList_extraInitializers);
70
63
  __esDecorate(this, null, _currentWidget_decorators, { kind: "accessor", name: "currentWidget", static: false, private: false, access: { has: obj => "currentWidget" in obj, get: obj => obj.currentWidget, set: (obj, value) => { obj.currentWidget = value; } }, metadata: _metadata }, _currentWidget_initializers, _currentWidget_extraInitializers);
71
- __esDecorate(this, null, _register_decorators, { kind: "method", name: "register", static: false, private: false, access: { has: obj => "register" in obj, get: obj => obj.register }, metadata: _metadata }, null, _instanceExtraInitializers);
72
- __esDecorate(this, null, _unregister_decorators, { kind: "method", name: "unregister", static: false, private: false, access: { has: obj => "unregister" in obj, get: obj => obj.unregister }, metadata: _metadata }, null, _instanceExtraInitializers);
73
64
  __esDecorate(this, null, _next_decorators, { kind: "method", name: "next", static: false, private: false, access: { has: obj => "next" in obj, get: obj => obj.next }, metadata: _metadata }, null, _instanceExtraInitializers);
74
65
  __esDecorate(this, null, _prev_decorators, { kind: "method", name: "prev", static: false, private: false, access: { has: obj => "prev" in obj, get: obj => obj.prev }, metadata: _metadata }, null, _instanceExtraInitializers);
75
66
  __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);
76
- __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);
67
+ __esDecorate(this, null, _settle_decorators, { kind: "method", name: "settle", static: false, private: false, access: { has: obj => "settle" in obj, get: obj => obj.settle }, metadata: _metadata }, null, _instanceExtraInitializers);
68
+ __esDecorate(this, null, _clear_decorators, { kind: "method", name: "clear", static: false, private: false, access: { has: obj => "clear" in obj, get: obj => obj.clear }, metadata: _metadata }, null, _instanceExtraInitializers);
77
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);
78
70
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
79
71
  }
80
- #widgetList_accessor_storage = (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _widgetList_initializers, []));
81
- get widgetList() { return this.#widgetList_accessor_storage; }
82
- set widgetList(value) { this.#widgetList_accessor_storage = value; }
83
- #currentWidget_accessor_storage = (__runInitializers(this, _widgetList_extraInitializers), __runInitializers(this, _currentWidget_initializers, null));
72
+ onScreen = __runInitializers(this, _instanceExtraInitializers);
73
+ #currentWidget_accessor_storage = __runInitializers(this, _currentWidget_initializers, null);
84
74
  get currentWidget() { return this.#currentWidget_accessor_storage; }
85
75
  set currentWidget(value) { this.#currentWidget_accessor_storage = value; }
86
76
  changeHandlers = (__runInitializers(this, _currentWidget_extraInitializers), new Set());
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.
83
+ */
84
+ constructor(onScreen) {
85
+ this.onScreen = onScreen;
86
+ }
87
87
  get current() {
88
88
  return this.currentWidget;
89
89
  }
90
90
  get widgets() {
91
- return this.widgetList;
92
- }
93
- register(widget) {
94
- // [LAW:one-source-of-truth] register is idempotent — the widgetList is
95
- // a set in spirit, not a multiset. Without this guard, double-registration
96
- // would duplicate the widget so next/prev cycle through it twice and
97
- // unregister only removes one copy.
98
- if (this.widgetList.includes(widget))
99
- return;
100
- this.widgetList = [...this.widgetList, widget];
101
- if (!this.currentWidget && widget.focusable && !widget.disabled) {
102
- this.setFocus(widget);
103
- }
104
- }
105
- unregister(widget) {
106
- const idx = this.widgetList.indexOf(widget);
107
- if (idx === -1)
108
- return;
109
- this.widgetList = this.widgetList.filter((w) => w !== widget);
110
- if (this.currentWidget === widget) {
111
- // [LAW:single-enforcer] WidgetBase.focus()/blur() already route through
112
- // handleFocus, so calling both would double-dispatch and trigger any
113
- // subclass side effect (e.g. Dropdown clearing filter/overlay) twice.
114
- widget.blur();
115
- const next = this.widgetList.find((w) => w.focusable && !w.disabled) ?? null;
116
- if (next)
117
- next.focus();
118
- this.currentWidget = next;
119
- this.emitChange();
120
- }
91
+ return this.onScreen().filter(takesFocus);
121
92
  }
122
93
  next() {
123
- const focusable = this.focusableWidgets();
94
+ const focusable = this.widgets;
124
95
  if (focusable.length === 0)
125
96
  return;
126
97
  const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
@@ -128,28 +99,42 @@ let DefaultFocusManager = (() => {
128
99
  this.setFocus(focusable[nextIdx]);
129
100
  }
130
101
  prev() {
131
- const focusable = this.focusableWidgets();
102
+ const focusable = this.widgets;
132
103
  if (focusable.length === 0)
133
104
  return;
134
105
  const currentIdx = this.currentWidget ? focusable.indexOf(this.currentWidget) : -1;
135
106
  const prevIdx = currentIdx <= 0 ? focusable.length - 1 : currentIdx - 1;
136
107
  this.setFocus(focusable[prevIdx]);
137
108
  }
109
+ // A widget not on screen yet can take focus: an app that is about to show
110
+ // it focuses it first, and the frame that shows it keeps it (`settle`).
138
111
  focus(widget) {
139
- if (!widget.focusable || widget.disabled)
140
- return;
141
- if (!this.widgetList.includes(widget))
112
+ if (!takesFocus(widget))
142
113
  return;
143
114
  this.setFocus(widget);
144
115
  }
145
- blur() {
146
- if (!this.currentWidget)
116
+ /**
117
+ * 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.
120
+ *
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.
124
+ *
125
+ * [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
127
+ * or was disabled.
128
+ */
129
+ settle(drawn) {
130
+ const current = this.currentWidget;
131
+ if (current && takesFocus(current) && drawn.includes(current))
147
132
  return;
148
- // [LAW:single-enforcer] blur() already dispatches handleFocus on
149
- // WidgetBase — see unregister() for the rationale.
150
- this.currentWidget.blur();
151
- this.currentWidget = null;
152
- this.emitChange();
133
+ const first = this.widgets[0];
134
+ if (first)
135
+ this.setFocus(first);
136
+ else
137
+ this.clear();
153
138
  }
154
139
  onChange(handler) {
155
140
  this.changeHandlers.add(handler);
@@ -169,14 +154,24 @@ let DefaultFocusManager = (() => {
169
154
  event.stop();
170
155
  }
171
156
  // --- Private ---
172
- focusableWidgets() {
173
- return this.widgetList.filter((w) => w.focusable && !w.disabled);
157
+ // No focus is where `settle` leaves an app with nothing to focus, and only
158
+ // there: anywhere else the next frame would put focus back on the first
159
+ // widget, so there is no public way in.
160
+ clear() {
161
+ if (!this.currentWidget)
162
+ return;
163
+ // [LAW:single-enforcer] WidgetBase.focus()/blur() route through
164
+ // handleFocus; calling it here as well would dispatch twice and run any
165
+ // subclass side effect (e.g. Dropdown clearing its filter) twice.
166
+ this.currentWidget.blur();
167
+ this.currentWidget = null;
168
+ this.emitChange();
174
169
  }
175
170
  setFocus(widget) {
176
171
  if (this.currentWidget === widget)
177
172
  return;
178
173
  // [LAW:single-enforcer] focus()/blur() already dispatch handleFocus —
179
- // see unregister() for the rationale.
174
+ // see clear() for the rationale.
180
175
  if (this.currentWidget)
181
176
  this.currentWidget.blur();
182
177
  this.currentWidget = widget;
@@ -191,3 +186,6 @@ let DefaultFocusManager = (() => {
191
186
  };
192
187
  })();
193
188
  export { DefaultFocusManager };
189
+ function takesFocus(widget) {
190
+ return widget.focusable && !widget.disabled;
191
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * widgetAt — which widget drew a cell of a painted frame, and where in its
3
+ * own output that cell sits; scrollTargetAt — what the wheel over that cell
4
+ * moves.
5
+ *
6
+ * A widget stamps every cell it draws (`WidgetBase.render`), and a
7
+ * `Scrollable` such as `Viewport` every cell it shows, around its content's
8
+ * stamps. The stamp rides
9
+ * through every container on the cell's `Style`, and the frame a screen paints
10
+ * is therefore its own hit map: nothing lays widgets out a second time to
11
+ * answer "what is under the pointer", so the answer cannot disagree with what
12
+ * the user sees. Z-order is paint order — whoever painted a cell last owns it.
13
+ */
14
+ import { Segment } from "../core/segment.js";
15
+ import type { Scrollable } from "../core/protocol.js";
16
+ import type { InteractiveWidget } from "./types.js";
17
+ /** A pointer position resolved to the widget that drew it. */
18
+ export interface WidgetHit {
19
+ readonly widget: InteractiveWidget;
20
+ /** Column of the cell in the widget's own output. */
21
+ readonly col: number;
22
+ /** Row of the cell in the widget's own output. */
23
+ readonly row: number;
24
+ }
25
+ /**
26
+ * The innermost widget that drew the cell at column `x` of row `y` of
27
+ * `frame`, or `undefined` when no widget drew it. A widget nested inside
28
+ * another widget's output wins over the one containing it.
29
+ */
30
+ export declare function widgetAt(frame: readonly (readonly Segment[])[], x: number, y: number): WidgetHit | undefined;
31
+ /**
32
+ * The innermost `Scrollable` that drew the cell at column `x` of row `y` of
33
+ * `frame` and that `scrollBy(lines)` would move, or `undefined` when none
34
+ * did. A widget drawn inside it does not hide it: the widget's anchor is
35
+ * nested in its. One that cannot move that way — a viewport whose content
36
+ * fits, or that is at that end — passes the wheel to the one around it.
37
+ */
38
+ export declare function scrollTargetAt(frame: readonly (readonly Segment[])[], x: number, y: number, lines: number): Scrollable | undefined;
39
+ /**
40
+ * Where `widget`'s output starts in `frame` — the cell its own row 0, column
41
+ * 0 falls on — read off the first cell it drew, or `undefined` when it drew
42
+ * none.
43
+ */
44
+ export declare function originOf(frame: readonly (readonly Segment[])[], widget: InteractiveWidget): {
45
+ x: number;
46
+ y: number;
47
+ } | undefined;
48
+ /**
49
+ * Whether `widget` drew the cell at column `x` of row `y` of `frame`, itself
50
+ * or through a widget nested in its output.
51
+ */
52
+ export declare function drew(frame: readonly (readonly Segment[])[], widget: InteractiveWidget, x: number, y: number): boolean;
53
+ /**
54
+ * Where `widget`'s footprint sits in `frame`: the cell its row 0, column 0
55
+ * falls on, and how many of its rows the frame shows down to its lowest — or
56
+ * `undefined` when the frame does not show its row 0.
57
+ */
58
+ export declare function footprintOf(frame: readonly (readonly Segment[])[], widget: InteractiveWidget): {
59
+ x: number;
60
+ y: number;
61
+ rows: number;
62
+ } | undefined;
63
+ /** Every owner that drew a cell of `frame`, at every depth of nesting. */
64
+ export declare function ownersOn(frame: readonly (readonly Segment[])[]): Set<object>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * widgetAt — which widget drew a cell of a painted frame, and where in its
3
+ * own output that cell sits; scrollTargetAt — what the wheel over that cell
4
+ * moves.
5
+ *
6
+ * A widget stamps every cell it draws (`WidgetBase.render`), and a
7
+ * `Scrollable` such as `Viewport` every cell it shows, around its content's
8
+ * stamps. The stamp rides
9
+ * through every container on the cell's `Style`, and the frame a screen paints
10
+ * is therefore its own hit map: nothing lays widgets out a second time to
11
+ * answer "what is under the pointer", so the answer cannot disagree with what
12
+ * the user sees. Z-order is paint order — whoever painted a cell last owns it.
13
+ */
14
+ import { Segment } from "../core/segment.js";
15
+ import { isScrollable } from "../core/protocol.js";
16
+ import { WidgetBase } from "./widget-base.js";
17
+ /**
18
+ * The innermost widget that drew the cell at column `x` of row `y` of
19
+ * `frame`, or `undefined` when no widget drew it. A widget nested inside
20
+ * another widget's output wins over the one containing it.
21
+ */
22
+ export function widgetAt(frame, x, y) {
23
+ const hit = innermost(frame, x, y, (owner) => (owner instanceof WidgetBase ? owner : undefined));
24
+ return hit && { widget: hit.owner, col: hit.col, row: hit.row };
25
+ }
26
+ /**
27
+ * The innermost `Scrollable` that drew the cell at column `x` of row `y` of
28
+ * `frame` and that `scrollBy(lines)` would move, or `undefined` when none
29
+ * did. A widget drawn inside it does not hide it: the widget's anchor is
30
+ * nested in its. One that cannot move that way — a viewport whose content
31
+ * fits, or that is at that end — passes the wheel to the one around it.
32
+ */
33
+ export function scrollTargetAt(frame, x, y, lines) {
34
+ return innermost(frame, x, y, (owner) => isScrollable(owner) && owner.canScrollBy(lines) ? owner : undefined)?.owner;
35
+ }
36
+ /**
37
+ * The innermost owner of the cell at column `x` of row `y` of `frame` that
38
+ * `accept` turns into a `T`, with the cell's place in that owner's output.
39
+ *
40
+ * [LAW:parse-dont-validate] An anchor's owner is any object; `accept` is
41
+ * where it becomes the kind of owner the caller asked for, or nothing.
42
+ */
43
+ function innermost(frame, x, y, accept) {
44
+ let found;
45
+ for (let a = Segment.anchorAt(frame, x, y); a; a = a.inner) {
46
+ const owner = accept(a.owner);
47
+ if (owner)
48
+ found = { owner, col: a.col, row: a.row };
49
+ }
50
+ return found;
51
+ }
52
+ /**
53
+ * Where `widget`'s output starts in `frame` — the cell its own row 0, column
54
+ * 0 falls on — read off the first cell it drew, or `undefined` when it drew
55
+ * none.
56
+ */
57
+ export function originOf(frame, widget) {
58
+ for (let y = 0; y < frame.length; y++) {
59
+ let start = 0;
60
+ for (const segment of frame[y]) {
61
+ for (let a = segment.style?.anchor; a; a = a.inner) {
62
+ if (a.owner === widget)
63
+ return { x: start - a.col, y: y - a.row };
64
+ }
65
+ start += segment.cellLength;
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+ /**
71
+ * Whether `widget` drew the cell at column `x` of row `y` of `frame`, itself
72
+ * or through a widget nested in its output.
73
+ */
74
+ export function drew(frame, widget, x, y) {
75
+ for (let a = Segment.anchorAt(frame, x, y); a; a = a.inner)
76
+ if (a.owner === widget)
77
+ return true;
78
+ return false;
79
+ }
80
+ /**
81
+ * Where `widget`'s footprint sits in `frame`: the cell its row 0, column 0
82
+ * falls on, and how many of its rows the frame shows down to its lowest — or
83
+ * `undefined` when the frame does not show its row 0.
84
+ */
85
+ export function footprintOf(frame, widget) {
86
+ let origin;
87
+ let rows = 0;
88
+ for (let y = 0; y < frame.length; y++) {
89
+ let start = 0;
90
+ for (const segment of frame[y]) {
91
+ for (let a = segment.style?.anchor; a; a = a.inner) {
92
+ if (a.owner !== widget)
93
+ continue;
94
+ if (a.row === 0 && !origin)
95
+ origin = { x: start - a.col, y };
96
+ rows = Math.max(rows, a.row + 1);
97
+ }
98
+ start += segment.cellLength;
99
+ }
100
+ }
101
+ return origin && { ...origin, rows };
102
+ }
103
+ /** Every owner that drew a cell of `frame`, at every depth of nesting. */
104
+ export function ownersOn(frame) {
105
+ const owners = new Set();
106
+ for (const line of frame) {
107
+ for (const segment of line) {
108
+ for (let a = segment.style?.anchor; a; a = a.inner)
109
+ owners.add(a.owner);
110
+ }
111
+ }
112
+ return owners;
113
+ }
@@ -1,11 +1,9 @@
1
- export type { KeyEventInit, KeyHandlerPriority, KeyHandlerOptions, WidgetMouseEvent, WidgetFocusEvent, WidgetBounds, InteractiveWidget, OverlayRenderable, FocusManager, Screen, MountEntry, Placement, } from "./types.js";
2
- export { FLOW, hasOverlay, KeyEvent } from "./types.js";
1
+ export type { KeyEventInit, KeyHandlerPriority, KeyHandlerOptions, ScreenMouseEvent, WidgetMouseEvent, WidgetFocusEvent, InteractiveWidget, OverlayRenderable, FocusManager, } from "./types.js";
2
+ export { hasOverlay, KeyEvent } from "./types.js";
3
3
  export { StaticItem } from "./static-item.js";
4
4
  export type { StaticItemOptions } from "./static-item.js";
5
5
  export { WidgetBase } from "./widget-base.js";
6
- export { DefaultFocusManager } from "./focus-manager.js";
7
- export { DefaultScreen } from "./screen.js";
8
- export type { ScreenOptions, ColorSystemSpec } from "./screen.js";
6
+ export { WidgetApp } from "./widget-app.js";
9
7
  export { Button } from "./button.js";
10
8
  export type { ButtonVariant, ButtonOptions } from "./button.js";
11
9
  export { Checkbox } from "./checkbox.js";
@@ -18,5 +16,5 @@ export { Dropdown } from "./dropdown.js";
18
16
  export type { DropdownOptions } from "./dropdown.js";
19
17
  export { Slider } from "./slider.js";
20
18
  export type { SliderOptions } from "./slider.js";
21
- export { EventRouter } from "./event-router.js";
22
- export type { EventRouterOptions } from "./event-router.js";
19
+ export { widgetAt } from "./hit.js";
20
+ export type { WidgetHit } from "./hit.js";
@@ -1,15 +1,14 @@
1
- export { FLOW, hasOverlay, KeyEvent } from "./types.js";
1
+ export { hasOverlay, KeyEvent } from "./types.js";
2
2
  export { StaticItem } from "./static-item.js";
3
3
  export { WidgetBase } from "./widget-base.js";
4
- export { DefaultFocusManager } from "./focus-manager.js";
5
- export { DefaultScreen } from "./screen.js";
4
+ export { WidgetApp } from "./widget-app.js";
6
5
  export { Button } from "./button.js";
7
6
  export { Checkbox } from "./checkbox.js";
8
7
  export { Toggle } from "./toggle.js";
9
8
  export { TextInput, charGreedyWrap } from "./text-input.js";
10
9
  export { Dropdown } from "./dropdown.js";
11
10
  export { Slider } from "./slider.js";
12
- export { EventRouter } from "./event-router.js";
11
+ export { widgetAt } from "./hit.js";
13
12
  // The terminal seam is not re-exported here. It lives on the `host` subpath,
14
13
  // and a second path to `TerminalHost` would be the two-clocks problem the
15
14
  // subpath split exists to remove.
@@ -18,7 +18,7 @@
18
18
  * disabled — dim
19
19
  */
20
20
  import { Segment } from "../core/segment.js";
21
- import type { RenderOptions } from "../core/protocol.js";
21
+ import { type RenderOptions } from "../core/protocol.js";
22
22
  import type { TerminalTheme } from "../core/color.js";
23
23
  import { WidgetBase } from "./widget-base.js";
24
24
  import type { KeyEvent, WidgetMouseEvent } from "./types.js";
@@ -48,7 +48,7 @@ export declare class Slider extends WidgetBase {
48
48
  handleMouse(event: WidgetMouseEvent): void;
49
49
  private setValue;
50
50
  private setValueFromMouseX;
51
- render(options: RenderOptions): Iterable<Segment>;
51
+ protected draw(options: RenderOptions): Iterable<Segment>;
52
52
  measure(_options: RenderOptions): {
53
53
  minimum: number;
54
54
  maximum: number;
@@ -56,6 +56,7 @@ import { Segment } from "../core/segment.js";
56
56
  import { Style } from "../core/style.js";
57
57
  import { ColorSpec } from "../core/color.js";
58
58
  import { DEFAULT_TERMINAL_THEME } from "../themes/terminalThemes.js";
59
+ import { drawable } from "../core/protocol.js";
59
60
  import { WidgetBase } from "./widget-base.js";
60
61
  const DEFAULT_WIDTH = 20;
61
62
  let Slider = (() => {
@@ -206,20 +207,19 @@ let Slider = (() => {
206
207
  this.value = snapped;
207
208
  this.emitChange();
208
209
  }
210
+ // `x` is a column of this slider's own track, and runs outside 0..width
211
+ // when a drag carries the pointer past either end.
209
212
  setValueFromMouseX(x) {
210
- const b = this.bounds;
211
- if (!b)
212
- return;
213
213
  const cellsAvailable = Math.max(1, this.width - 1);
214
- const clamped = Math.max(0, Math.min(this.width - 1, x - b.x));
214
+ const clamped = Math.max(0, Math.min(this.width - 1, x));
215
215
  const fraction = clamped / cellsAvailable;
216
216
  const next = this.min + fraction * (this.max - this.min);
217
217
  this.setValue(next);
218
218
  }
219
219
  // --- Rendering ---
220
- render(options) {
221
- const trackChar = options.asciiOnly ? "-" : "─";
222
- const markerChar = options.asciiOnly ? "*" : "●";
220
+ draw(options) {
221
+ const trackChar = drawable(options, "─", "-");
222
+ const markerChar = drawable(options, "●", "*");
223
223
  const range = this.max - this.min;
224
224
  const fraction = range === 0 ? 0 : (this.value - this.min) / range;
225
225
  const markerIdx = Math.round(fraction * (this.width - 1));
@@ -1,20 +1,14 @@
1
1
  /**
2
- * StaticItem — a non-focusable, non-interactive Renderable wrapped to fit
3
- * the InteractiveWidget surface so it can be mounted by Screen alongside
4
- * real widgets.
2
+ * StaticItem — a renderable on the widget surface: it takes no focus and
3
+ * ignores keys, and it stamps its cells like any widget.
5
4
  *
6
- * [LAW:one-type-per-behavior] Screen.mount accepts a uniform array of
7
- * InteractiveWidget. Static text, panels, swatches, and other display-only
8
- * Renderables flow through the same pipeline by being expressed as the
9
- * same type. The differences (no focus, no event handling) are configured
10
- * via `focusable: false` plus the no-op handlers WidgetBase already
11
- * provides — there is no second type, no "static vs interactive" branch in
12
- * Screen's render loop.
5
+ * [LAW:one-type-per-behavior] Display-only content is a `WidgetBase` with
6
+ * `focusable: false` and the no-op handlers the base already provides, not a
7
+ * second type.
13
8
  *
14
- * The wrapped renderable can be a function that re-evaluates each frame
15
- * (so the host can read MobX observables inside `render` and have Screen
16
- * re-render reactively) or a plain object that returns the same segments
17
- * every time.
9
+ * The wrapped renderable can be a function that re-evaluates each frame — it
10
+ * reads MobX observables, and the app repaints when they change — or a plain
11
+ * renderable.
18
12
  */
19
13
  import { Segment } from "../core/segment.js";
20
14
  import type { Measurable, Renderable, RenderOptions } from "../core/protocol.js";
@@ -32,7 +26,7 @@ export declare class StaticItem extends WidgetBase {
32
26
  private readonly measureFn;
33
27
  constructor(options: StaticItemOptions);
34
28
  handleKey(_event: KeyEvent): void;
35
- render(options: RenderOptions): Iterable<Segment>;
29
+ protected draw(options: RenderOptions): Iterable<Segment>;
36
30
  measure(options: RenderOptions): {
37
31
  minimum: number;
38
32
  maximum: number;
@@ -1,20 +1,14 @@
1
1
  /**
2
- * StaticItem — a non-focusable, non-interactive Renderable wrapped to fit
3
- * the InteractiveWidget surface so it can be mounted by Screen alongside
4
- * real widgets.
2
+ * StaticItem — a renderable on the widget surface: it takes no focus and
3
+ * ignores keys, and it stamps its cells like any widget.
5
4
  *
6
- * [LAW:one-type-per-behavior] Screen.mount accepts a uniform array of
7
- * InteractiveWidget. Static text, panels, swatches, and other display-only
8
- * Renderables flow through the same pipeline by being expressed as the
9
- * same type. The differences (no focus, no event handling) are configured
10
- * via `focusable: false` plus the no-op handlers WidgetBase already
11
- * provides — there is no second type, no "static vs interactive" branch in
12
- * Screen's render loop.
5
+ * [LAW:one-type-per-behavior] Display-only content is a `WidgetBase` with
6
+ * `focusable: false` and the no-op handlers the base already provides, not a
7
+ * second type.
13
8
  *
14
- * The wrapped renderable can be a function that re-evaluates each frame
15
- * (so the host can read MobX observables inside `render` and have Screen
16
- * re-render reactively) or a plain object that returns the same segments
17
- * every time.
9
+ * The wrapped renderable can be a function that re-evaluates each frame — it
10
+ * reads MobX observables, and the app repaints when they change — or a plain
11
+ * renderable.
18
12
  */
19
13
  import { Segment } from "../core/segment.js";
20
14
  import { WidgetBase } from "./widget-base.js";
@@ -36,7 +30,7 @@ export class StaticItem extends WidgetBase {
36
30
  // Static items don't respond to keys. WidgetBase.handleMouse / handleFocus
37
31
  // are already no-ops, so we only need to satisfy the abstract handleKey.
38
32
  handleKey(_event) { }
39
- render(options) {
33
+ draw(options) {
40
34
  return this.renderFn(options);
41
35
  }
42
36
  measure(options) {
@@ -58,7 +58,7 @@
58
58
  import { Segment } from "../core/segment.js";
59
59
  import { Style } from "../core/style.js";
60
60
  import { type CellCol, type CodePoint } from "../core/cells.js";
61
- import type { RenderOptions } from "../core/protocol.js";
61
+ import { type RenderOptions } from "../core/protocol.js";
62
62
  import type { TerminalTheme } from "../core/color.js";
63
63
  import { WidgetBase } from "./widget-base.js";
64
64
  import type { KeyEvent, WidgetMouseEvent } from "./types.js";
@@ -247,7 +247,7 @@ export declare class TextInput extends WidgetBase {
247
247
  private _lineStart;
248
248
  private _lineEnd;
249
249
  private _insertText;
250
- render(options: RenderOptions): Iterable<Segment>;
250
+ protected draw(options: RenderOptions): Iterable<Segment>;
251
251
  private _renderSingleLine;
252
252
  private _renderMultiline;
253
253
  private _emitRowContent;