@promptctl/rich-js 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +13 -26
  2. package/package.json +7 -3
  3. package/dist/core/box.d.ts +0 -144
  4. package/dist/core/box.js +0 -391
  5. package/dist/core/cells.d.ts +0 -148
  6. package/dist/core/cells.js +0 -266
  7. package/dist/core/color.d.ts +0 -250
  8. package/dist/core/color.js +0 -1029
  9. package/dist/core/console.d.ts +0 -156
  10. package/dist/core/console.js +0 -472
  11. package/dist/core/emoji.d.ts +0 -24
  12. package/dist/core/emoji.js +0 -391
  13. package/dist/core/export-html.d.ts +0 -24
  14. package/dist/core/export-html.js +0 -106
  15. package/dist/core/export-lines.d.ts +0 -121
  16. package/dist/core/export-lines.js +0 -141
  17. package/dist/core/highlighter.d.ts +0 -33
  18. package/dist/core/highlighter.js +0 -124
  19. package/dist/core/json.d.ts +0 -27
  20. package/dist/core/json.js +0 -53
  21. package/dist/core/markup.d.ts +0 -97
  22. package/dist/core/markup.js +0 -619
  23. package/dist/core/measure.d.ts +0 -27
  24. package/dist/core/measure.js +0 -61
  25. package/dist/core/oklch.d.ts +0 -161
  26. package/dist/core/oklch.js +0 -312
  27. package/dist/core/osc8.d.ts +0 -68
  28. package/dist/core/osc8.js +0 -90
  29. package/dist/core/pretty.d.ts +0 -141
  30. package/dist/core/pretty.js +0 -472
  31. package/dist/core/protocol.d.ts +0 -137
  32. package/dist/core/protocol.js +0 -101
  33. package/dist/core/render.d.ts +0 -84
  34. package/dist/core/render.js +0 -145
  35. package/dist/core/segment.d.ts +0 -142
  36. package/dist/core/segment.js +0 -433
  37. package/dist/core/spinnerData.d.ts +0 -10
  38. package/dist/core/spinnerData.js +0 -115
  39. package/dist/core/strip.d.ts +0 -145
  40. package/dist/core/strip.js +0 -308
  41. package/dist/core/style.d.ts +0 -144
  42. package/dist/core/style.js +0 -723
  43. package/dist/core/subscription.d.ts +0 -12
  44. package/dist/core/subscription.js +0 -12
  45. package/dist/core/text.d.ts +0 -255
  46. package/dist/core/text.js +0 -990
  47. package/dist/core/wrap.d.ts +0 -41
  48. package/dist/core/wrap.js +0 -127
  49. package/dist/host/host-stream.d.ts +0 -30
  50. package/dist/host/host-stream.js +0 -35
  51. package/dist/host/index.d.ts +0 -3
  52. package/dist/host/index.js +0 -2
  53. package/dist/host/terminal-host.d.ts +0 -143
  54. package/dist/host/terminal-host.js +0 -132
  55. package/dist/index.d.ts +0 -87
  56. package/dist/index.js +0 -104
  57. package/dist/node/prompt.d.ts +0 -21
  58. package/dist/node/prompt.js +0 -30
  59. package/dist/node/save.d.ts +0 -17
  60. package/dist/node/save.js +0 -21
  61. package/dist/node/terminal-host.d.ts +0 -84
  62. package/dist/node/terminal-host.js +0 -160
  63. package/dist/node/traceback.d.ts +0 -27
  64. package/dist/node/traceback.js +0 -112
  65. package/dist/renderables/align.d.ts +0 -16
  66. package/dist/renderables/align.js +0 -46
  67. package/dist/renderables/columns.d.ts +0 -61
  68. package/dist/renderables/columns.js +0 -168
  69. package/dist/renderables/constrain.d.ts +0 -15
  70. package/dist/renderables/constrain.js +0 -37
  71. package/dist/renderables/flexStrip.d.ts +0 -41
  72. package/dist/renderables/flexStrip.js +0 -200
  73. package/dist/renderables/group.d.ts +0 -11
  74. package/dist/renderables/group.js +0 -15
  75. package/dist/renderables/layout.d.ts +0 -92
  76. package/dist/renderables/layout.js +0 -315
  77. package/dist/renderables/live.d.ts +0 -47
  78. package/dist/renderables/live.js +0 -131
  79. package/dist/renderables/markdown.d.ts +0 -24
  80. package/dist/renderables/markdown.js +0 -182
  81. package/dist/renderables/padding.d.ts +0 -38
  82. package/dist/renderables/padding.js +0 -140
  83. package/dist/renderables/panel.d.ts +0 -116
  84. package/dist/renderables/panel.js +0 -322
  85. package/dist/renderables/progress.d.ts +0 -90
  86. package/dist/renderables/progress.js +0 -241
  87. package/dist/renderables/progressBar.d.ts +0 -31
  88. package/dist/renderables/progressBar.js +0 -52
  89. package/dist/renderables/prompt.d.ts +0 -49
  90. package/dist/renderables/prompt.js +0 -125
  91. package/dist/renderables/rule.d.ts +0 -24
  92. package/dist/renderables/rule.js +0 -90
  93. package/dist/renderables/spinner.d.ts +0 -29
  94. package/dist/renderables/spinner.js +0 -64
  95. package/dist/renderables/status.d.ts +0 -23
  96. package/dist/renderables/status.js +0 -60
  97. package/dist/renderables/syntax.d.ts +0 -35
  98. package/dist/renderables/syntax.js +0 -114
  99. package/dist/renderables/table.d.ts +0 -182
  100. package/dist/renderables/table.js +0 -697
  101. package/dist/renderables/traceback.d.ts +0 -31
  102. package/dist/renderables/traceback.js +0 -109
  103. package/dist/renderables/tree.d.ts +0 -54
  104. package/dist/renderables/tree.js +0 -159
  105. package/dist/template-bindings/color-funcs.d.ts +0 -87
  106. package/dist/template-bindings/color-funcs.js +0 -219
  107. package/dist/template-bindings/helpers.d.ts +0 -24
  108. package/dist/template-bindings/helpers.js +0 -48
  109. package/dist/template-bindings/index.d.ts +0 -107
  110. package/dist/template-bindings/index.js +0 -144
  111. package/dist/template-bindings/palette-funcs.d.ts +0 -94
  112. package/dist/template-bindings/palette-funcs.js +0 -151
  113. package/dist/template-bindings/style-funcs.d.ts +0 -28
  114. package/dist/template-bindings/style-funcs.js +0 -151
  115. package/dist/themes/buildPalette.d.ts +0 -36
  116. package/dist/themes/buildPalette.js +0 -56
  117. package/dist/themes/colorMath.d.ts +0 -82
  118. package/dist/themes/colorMath.js +0 -254
  119. package/dist/themes/colorRef.d.ts +0 -51
  120. package/dist/themes/colorRef.js +0 -96
  121. package/dist/themes/data/atom-one-dark.d.ts +0 -3
  122. package/dist/themes/data/atom-one-dark.js +0 -158
  123. package/dist/themes/data/atom-one-light.d.ts +0 -3
  124. package/dist/themes/data/atom-one-light.js +0 -158
  125. package/dist/themes/data/catppuccin-frappe.d.ts +0 -3
  126. package/dist/themes/data/catppuccin-frappe.js +0 -158
  127. package/dist/themes/data/catppuccin-latte.d.ts +0 -3
  128. package/dist/themes/data/catppuccin-latte.js +0 -158
  129. package/dist/themes/data/catppuccin-macchiato.d.ts +0 -3
  130. package/dist/themes/data/catppuccin-macchiato.js +0 -158
  131. package/dist/themes/data/catppuccin-mocha.d.ts +0 -3
  132. package/dist/themes/data/catppuccin-mocha.js +0 -158
  133. package/dist/themes/data/cyberpunk.d.ts +0 -17
  134. package/dist/themes/data/cyberpunk.js +0 -191
  135. package/dist/themes/data/default.d.ts +0 -3
  136. package/dist/themes/data/default.js +0 -158
  137. package/dist/themes/data/dracula.d.ts +0 -3
  138. package/dist/themes/data/dracula.js +0 -158
  139. package/dist/themes/data/flexoki.d.ts +0 -3
  140. package/dist/themes/data/flexoki.js +0 -158
  141. package/dist/themes/data/gruvbox.d.ts +0 -3
  142. package/dist/themes/data/gruvbox.js +0 -158
  143. package/dist/themes/data/index.d.ts +0 -35
  144. package/dist/themes/data/index.js +0 -64
  145. package/dist/themes/data/monokai.d.ts +0 -3
  146. package/dist/themes/data/monokai.js +0 -158
  147. package/dist/themes/data/nord.d.ts +0 -3
  148. package/dist/themes/data/nord.js +0 -158
  149. package/dist/themes/data/rose-pine-dawn.d.ts +0 -3
  150. package/dist/themes/data/rose-pine-dawn.js +0 -158
  151. package/dist/themes/data/rose-pine-moon.d.ts +0 -3
  152. package/dist/themes/data/rose-pine-moon.js +0 -158
  153. package/dist/themes/data/rose-pine.d.ts +0 -3
  154. package/dist/themes/data/rose-pine.js +0 -158
  155. package/dist/themes/data/solarized-dark.d.ts +0 -3
  156. package/dist/themes/data/solarized-dark.js +0 -158
  157. package/dist/themes/data/solarized-light.d.ts +0 -3
  158. package/dist/themes/data/solarized-light.js +0 -158
  159. package/dist/themes/data/svg-export.d.ts +0 -3
  160. package/dist/themes/data/svg-export.js +0 -158
  161. package/dist/themes/data/textual-ansi.d.ts +0 -3
  162. package/dist/themes/data/textual-ansi.js +0 -158
  163. package/dist/themes/data/textual-dark.d.ts +0 -3
  164. package/dist/themes/data/textual-dark.js +0 -158
  165. package/dist/themes/data/textual-light.d.ts +0 -3
  166. package/dist/themes/data/textual-light.js +0 -158
  167. package/dist/themes/data/tokyo-night.d.ts +0 -3
  168. package/dist/themes/data/tokyo-night.js +0 -158
  169. package/dist/themes/data/types.d.ts +0 -15
  170. package/dist/themes/data/types.js +0 -1
  171. package/dist/themes/palette.d.ts +0 -21
  172. package/dist/themes/palette.js +0 -28
  173. package/dist/themes/ramp.d.ts +0 -83
  174. package/dist/themes/ramp.js +0 -122
  175. package/dist/themes/registry.d.ts +0 -43
  176. package/dist/themes/registry.js +0 -82
  177. package/dist/themes/terminalThemes.d.ts +0 -54
  178. package/dist/themes/terminalThemes.js +0 -68
  179. package/dist/themes/transpose.d.ts +0 -87
  180. package/dist/themes/transpose.js +0 -138
  181. package/dist/widgets/button.d.ts +0 -49
  182. package/dist/widgets/button.js +0 -206
  183. package/dist/widgets/checkbox.d.ts +0 -41
  184. package/dist/widgets/checkbox.js +0 -155
  185. package/dist/widgets/dropdown.d.ts +0 -67
  186. package/dist/widgets/dropdown.js +0 -453
  187. package/dist/widgets/event-router.d.ts +0 -78
  188. package/dist/widgets/event-router.js +0 -622
  189. package/dist/widgets/focus-manager.d.ts +0 -26
  190. package/dist/widgets/focus-manager.js +0 -193
  191. package/dist/widgets/index.d.ts +0 -22
  192. package/dist/widgets/index.js +0 -15
  193. package/dist/widgets/screen.d.ts +0 -98
  194. package/dist/widgets/screen.js +0 -530
  195. package/dist/widgets/slider.d.ts +0 -57
  196. package/dist/widgets/slider.js +0 -270
  197. package/dist/widgets/static-item.d.ts +0 -40
  198. package/dist/widgets/static-item.js +0 -60
  199. package/dist/widgets/text-input.d.ts +0 -274
  200. package/dist/widgets/text-input.js +0 -1162
  201. package/dist/widgets/toggle.d.ts +0 -44
  202. package/dist/widgets/toggle.js +0 -179
  203. package/dist/widgets/types.d.ts +0 -108
  204. package/dist/widgets/types.js +0 -31
  205. package/dist/widgets/widget-base.d.ts +0 -42
  206. package/dist/widgets/widget-base.js +0 -173
@@ -1,530 +0,0 @@
1
- /**
2
- * Screen — owns the render loop and ANSI output for interactive widgets.
3
- *
4
- * [LAW:dataflow-not-control-flow] Same pipeline runs every frame: render
5
- * widgets to Segment[], split into lines, encode to ANSI, write to stdout
6
- * with cursor-up + overwrite. Variability lives in the values (which widgets
7
- * are visible, what they render), never in whether the pipeline runs.
8
- *
9
- * [LAW:one-source-of-truth] Layout (widget bounds) is computed here and
10
- * written to widget.bounds. Mouse hit-testing reads the same bounds — the
11
- * Screen is the single authority on where each widget is drawn.
12
- *
13
- * [LAW:single-enforcer] Cursor management lives in one place: `draw()`
14
- * tracks `lastLineCount` and emits a single `\x1b[<n>A` to reposition.
15
- * Per-line `\x1b[K` (erase to end of line) overwrites old content without
16
- * flicker — no clear-and-redraw cycle.
17
- *
18
- * Reactivity: `mobx.autorun` re-fires on any observable read during render
19
- * (label, focused, hovered, active, disabled, visible, the widget list
20
- * itself). Renders are debounced to a microtask so a burst of state changes
21
- * within one tick produces one frame.
22
- *
23
- * Layout (per-widget Placement):
24
- *
25
- * flow — vertical stack at x=0; advances the layout cursor by the
26
- * widget's measured height. Default placement; preserves the
27
- * historical single-column behavior so existing consumers
28
- * (and the screen tests) need no migration.
29
- * inline — same row as the preceding flow/inline neighbor; x packs
30
- * after that neighbor's right edge plus a one-cell gap. The
31
- * row's height is the max of its members; the cursor advances
32
- * past the tallest member when the next non-inline item is
33
- * placed (or at end of frame).
34
- * fixed — absolute (x, y); independent of the cursor. Used for
35
- * anchored content like status/log rows that the host wants
36
- * placed at a known coordinate regardless of flow growth.
37
- *
38
- * The overlay pass (single enforcer) runs after base layout: any widget
39
- * implementing OverlayRenderable contributes overlay segments anchored
40
- * directly below its inline footprint, and Screen unions the overlay
41
- * area into the widget's bounds for hit-testing.
42
- *
43
- * Design alternatives considered:
44
- * 1. Per-widget Placement (chosen) — smallest type that covers the legal
45
- * variability; one total switch in computeFrame; back-compat default.
46
- * 2. Named regions (header/body/status as flow containers) — adds a
47
- * two-step API surface (declare regions, then mount into them) and
48
- * another concept; rejected as larger than the problem requires.
49
- * 3. Host-supplied layout function — pushes layout out of Screen,
50
- * defeating single-enforcer; rejected.
51
- */
52
- var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
53
- function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
54
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
55
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
56
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
57
- var _, done = false;
58
- for (var i = decorators.length - 1; i >= 0; i--) {
59
- var context = {};
60
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
61
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
62
- context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
63
- var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
64
- if (kind === "accessor") {
65
- if (result === void 0) continue;
66
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
67
- if (_ = accept(result.get)) descriptor.get = _;
68
- if (_ = accept(result.set)) descriptor.set = _;
69
- if (_ = accept(result.init)) initializers.unshift(_);
70
- }
71
- else if (_ = accept(result)) {
72
- if (kind === "field") initializers.unshift(_);
73
- else descriptor[key] = _;
74
- }
75
- }
76
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
77
- done = true;
78
- };
79
- var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
80
- var useValue = arguments.length > 2;
81
- for (var i = 0; i < initializers.length; i++) {
82
- value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
83
- }
84
- return useValue ? value : void 0;
85
- };
86
- import { autorun, observableShallow, runInAction } from "mobx";
87
- import { Segment } from "../core/segment.js";
88
- import { asCellCol } from "../core/cells.js";
89
- import { segmentsToString } from "../core/render.js";
90
- import { resolveDestination } from "../core/color.js";
91
- import { DefaultFocusManager } from "./focus-manager.js";
92
- import { FLOW, hasOverlay } from "./types.js";
93
- let DefaultScreen = (() => {
94
- let _widgetList_decorators;
95
- let _widgetList_initializers = [];
96
- let _widgetList_extraInitializers = [];
97
- return class DefaultScreen {
98
- static {
99
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
100
- _widgetList_decorators = [observableShallow];
101
- __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);
102
- if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
103
- }
104
- focusManager;
105
- #widgetList_accessor_storage = __runInitializers(this, _widgetList_initializers, []);
106
- // [LAW:one-source-of-truth] The mounted widget list is observable so
107
- // mount/unmount triggers the render autorun without an extra subscription.
108
- get widgetList() { return this.#widgetList_accessor_storage; }
109
- set widgetList(value) { this.#widgetList_accessor_storage = value; }
110
- // [LAW:one-source-of-truth] Placements live in a parallel Map keyed by
111
- // widget. Placements are immutable per mount — Screen sets them and never
112
- // mutates them, so they don't need to be observable. The observable
113
- // `widgetList` above already fires reactivity on mount/unmount.
114
- placements = (__runInitializers(this, _widgetList_extraInitializers), new Map());
115
- // [LAW:single-enforcer] Set of widgets whose overlay rendered visible
116
- // rows in the most recent frame. Updated by draw() from FrameLayout;
117
- // consumed by the `widgets` getter to expose hit-test z-order to
118
- // EventRouter. Mount/unmount don't touch this — it's a per-frame
119
- // derivation, not mount state.
120
- _activeOverlays = new Set();
121
- _running = false;
122
- autorunDispose;
123
- renderScheduled = false;
124
- // [LAW:one-source-of-truth] The autorun computes the next frame once and
125
- // stores it here; draw() reads and clears. Not observable — only the
126
- // autorun writes, only draw reads, both run in the same single-threaded
127
- // event loop.
128
- pendingFrame = null;
129
- lastLineCount = 0;
130
- host;
131
- widthOverride;
132
- destination;
133
- manageCursor;
134
- constructor(options) {
135
- this.host = options.host;
136
- this.widthOverride = options.width;
137
- this.focusManager = options.focusManager ?? new DefaultFocusManager();
138
- const isTTY = this.host.isTTY;
139
- this.destination = resolveDestination(options.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY });
140
- this.manageCursor = options.manageCursor ?? isTTY;
141
- }
142
- get running() {
143
- return this._running;
144
- }
145
- // [LAW:types-are-the-program] `widgets` is the *output* of layout: the
146
- // list EventRouter uses for hit-testing, in z-order — non-overlay widgets
147
- // first (in mount order), overlay-active widgets last. `topmostHit`
148
- // iterates this in reverse and returns the first containing widget, so
149
- // overlay-active widgets win against anything mounted under them.
150
- // [LAW:single-enforcer] Screen is the sole authority on z-order; the
151
- // router consumes the result, never derives it itself.
152
- get widgets() {
153
- if (this._activeOverlays.size === 0)
154
- return this.widgetList;
155
- const base = [];
156
- const top = [];
157
- for (const w of this.widgetList) {
158
- (this._activeOverlays.has(w) ? top : base).push(w);
159
- }
160
- return [...base, ...top];
161
- }
162
- // --- Lifecycle ---
163
- mount(...entries) {
164
- runInAction(() => {
165
- const next = [...this.widgetList];
166
- for (const entry of entries) {
167
- const { widget, placement } = normalizeEntry(entry);
168
- if (next.includes(widget))
169
- continue;
170
- next.push(widget);
171
- this.placements.set(widget, placement);
172
- this.focusManager.register(widget);
173
- }
174
- this.widgetList = next;
175
- });
176
- }
177
- unmount(widget) {
178
- runInAction(() => {
179
- const idx = this.widgetList.indexOf(widget);
180
- if (idx === -1)
181
- return;
182
- this.widgetList = this.widgetList.filter((w) => w !== widget);
183
- this.placements.delete(widget);
184
- this.focusManager.unregister(widget);
185
- // [LAW:one-source-of-truth] Drop any z-order entry for the removed
186
- // widget so a later getter doesn't return a dangling reference.
187
- // The next draw will rebuild this from scratch.
188
- this._activeOverlays.delete(widget);
189
- });
190
- }
191
- start() {
192
- if (this._running)
193
- return;
194
- this._running = true;
195
- this.lastLineCount = 0;
196
- if (this.manageCursor)
197
- this.host.write("\x1b[?25l");
198
- // [LAW:dataflow-not-control-flow] One reactive pipeline. The autorun
199
- // computes the frame (which reads observables; MobX subscribes to
200
- // exactly what was read so only relevant state changes trigger redraws),
201
- // caches it as `pendingFrame`, and schedules a microtask to paint it.
202
- // draw() consumes the cached frame instead of recomputing — without the
203
- // cache, every tick laid the frame out twice (once for subscription,
204
- // once for paint).
205
- this.autorunDispose = autorun(() => {
206
- this.pendingFrame = this.computeFrame();
207
- this.scheduleRender();
208
- });
209
- }
210
- stop() {
211
- if (!this._running)
212
- return;
213
- this._running = false;
214
- if (this.autorunDispose) {
215
- this.autorunDispose();
216
- this.autorunDispose = undefined;
217
- }
218
- // [LAW:one-source-of-truth] stop() returns the screen to a clean
219
- // baseline. Without these resets a stopped screen would retain the
220
- // last FrameLayout (segments + bounds) — unnecessary memory and a
221
- // restart hazard where the first frame after restart could leak the
222
- // pre-stop layout. Same idempotent-cleanup pattern as EventRouter.stop.
223
- this.pendingFrame = null;
224
- this.renderScheduled = false;
225
- this._activeOverlays = new Set();
226
- if (this.manageCursor)
227
- this.host.write("\x1b[?25h");
228
- if (this.lastLineCount > 0)
229
- this.host.write("\n");
230
- }
231
- // --- Internal: layout + draw ---
232
- get width() {
233
- if (this.widthOverride !== undefined)
234
- return this.widthOverride;
235
- return this.host.size().cols;
236
- }
237
- computeFrame() {
238
- // [LAW:one-source-of-truth] Snapshot the width ONCE at the top of
239
- // the frame so layout, per-widget clipping, per-line clipping, and
240
- // the final paint all see the same value. Reading `this.width`
241
- // multiple times can drift across a microtask boundary if the
242
- // terminal is resized between compute and draw.
243
- const width = this.width;
244
- const renderOptions = {
245
- maxWidth: width,
246
- isTerminal: true,
247
- encoding: "utf-8",
248
- // [LAW:one-source-of-truth] The depth the frame is encoded at (below,
249
- // `segmentsToString`), so a renderable that decides on drawn colours
250
- // decides on the ones this screen draws.
251
- colorSystem: this.destination.colorSystem,
252
- };
253
- const lines = [];
254
- const boundsList = [];
255
- // [LAW:single-enforcer] Populated by pass 2 below. Pass 2 is the only
256
- // place that decides whether a widget's overlay produced visible rows
257
- // this frame — exactly where the z-order discriminator belongs.
258
- const activeOverlays = new Set();
259
- // Layout cursor for flow placement. `cursorY` is the next free row.
260
- // `lastFlowRow` tracks the most recent flow/inline row so that a
261
- // following inline placement can pack against its right edge.
262
- let cursorY = 0;
263
- let lastFlowRow = null;
264
- // Pass 1 — base layout. Each widget contributes its inline footprint
265
- // (Renderable.render). Position is determined by the widget's Placement.
266
- for (const widget of this.widgetList) {
267
- // [LAW:dataflow-not-control-flow] Hidden widgets still pass through the
268
- // pipeline — their data just produces zero rows and zero-size bounds.
269
- // The same operations execute every iteration.
270
- const visible = widget.visible;
271
- const segments = visible ? Array.from(widget.render(renderOptions)) : [];
272
- const rawLines = visible ? Segment.splitLines(segments) : [];
273
- const placement = this.placements.get(widget) ?? FLOW;
274
- // [LAW:types-are-the-program] Single total switch on the discriminated
275
- // union: every legal placement gets exactly one branch, the compiler
276
- // enforces exhaustiveness, and the rest of the pipeline (clip to the
277
- // remaining screen width, paint into `lines`, record bounds) is
278
- // identical for every kind. x/y come from the placement; the available
279
- // width comes from `this.width - x`.
280
- let x;
281
- let y;
282
- let prevAtPlacement = null;
283
- switch (placement.kind) {
284
- case "flow":
285
- x = 0;
286
- y = cursorY;
287
- break;
288
- case "inline": {
289
- // Pack onto the most recent flow/inline row. If there is no prior
290
- // row (inline used as the first item), fall back to flow at x=0.
291
- prevAtPlacement = lastFlowRow;
292
- if (prevAtPlacement === null) {
293
- x = 0;
294
- y = cursorY;
295
- }
296
- else {
297
- x = prevAtPlacement.rightX + (prevAtPlacement.rightX > 0 ? 1 : 0);
298
- y = prevAtPlacement.startY;
299
- }
300
- break;
301
- }
302
- case "fixed":
303
- x = placement.x;
304
- y = placement.y;
305
- break;
306
- }
307
- // [LAW:single-enforcer] Clip widget lines to the remaining screen
308
- // width here so widget bounds match what actually gets painted. A
309
- // renderable that emits more cells than its allotted slot (rogue
310
- // widget, or an inline placement overflowing the screen edge) would
311
- // otherwise compute bounds wider than reality, breaking hit-testing
312
- // and the inline rightX that subsequent inline widgets pack against.
313
- const available = Math.max(0, width - x);
314
- const widgetLines = available > 0
315
- ? rawLines.map((line) => Segment.adjustLineLength(line, available, undefined, false))
316
- : [];
317
- const [w, h] = Segment.getShape(widgetLines);
318
- paintLines(lines, widgetLines, x, y);
319
- if (placement.kind === "flow" && h > 0) {
320
- cursorY = y + h;
321
- lastFlowRow = { startY: y, height: h, rightX: x + w };
322
- }
323
- else if (placement.kind === "inline" && h > 0) {
324
- const rowStart = prevAtPlacement?.startY ?? y;
325
- const rowHeight = Math.max(prevAtPlacement?.height ?? 0, h);
326
- cursorY = Math.max(cursorY, rowStart + rowHeight);
327
- lastFlowRow = { startY: rowStart, height: rowHeight, rightX: x + w };
328
- }
329
- // Fixed placements never advance the flow cursor — they are
330
- // independent anchors. paintLines grows the canvas as needed.
331
- boundsList.push({ widget, bounds: { x, y, width: w, height: h } });
332
- }
333
- // Pass 2 — overlays. Widgets that implement OverlayRenderable paint
334
- // ON TOP of the frame, anchored directly below their inline footprint.
335
- // Render order = z-order: the overlay pass runs last, so overlay rows
336
- // overwrite anything that was placed below the widget by pass 1.
337
- // [LAW:single-enforcer] Overlay placement, bounds-union, AND z-order
338
- // for hit-testing all happen here; widgets never draw their own
339
- // overlays and the router never re-derives z-order. Each widget that
340
- // produces visible overlay rows is added to `activeOverlays` so the
341
- // `widgets` getter can surface it as topmost.
342
- for (const entry of boundsList) {
343
- const widget = entry.widget;
344
- if (!widget.visible)
345
- continue;
346
- if (!hasOverlay(widget))
347
- continue;
348
- const overlaySegs = widget.renderOverlay(renderOptions);
349
- if (overlaySegs === null)
350
- continue;
351
- const overlayRawLines = Segment.splitLines(Array.from(overlaySegs));
352
- if (overlayRawLines.length === 0)
353
- continue;
354
- // [LAW:single-enforcer] Same per-line clip as the base pass — bounds
355
- // (used for hit-testing the overlay) match the painted output.
356
- const overlayAvailable = Math.max(0, width - entry.bounds.x);
357
- const overlayLines = overlayAvailable > 0
358
- ? overlayRawLines.map((line) => Segment.adjustLineLength(line, overlayAvailable, undefined, false))
359
- : [];
360
- if (overlayLines.length === 0)
361
- continue;
362
- const startY = entry.bounds.y + entry.bounds.height;
363
- paintLines(lines, overlayLines, entry.bounds.x, startY);
364
- const [overlayW] = Segment.getShape(overlayLines);
365
- entry.bounds = {
366
- x: entry.bounds.x,
367
- y: entry.bounds.y,
368
- width: Math.max(entry.bounds.width, overlayW),
369
- height: entry.bounds.height + overlayLines.length,
370
- };
371
- activeOverlays.add(widget);
372
- }
373
- return { width, lines, bounds: boundsList, activeOverlays };
374
- }
375
- scheduleRender() {
376
- if (this.renderScheduled)
377
- return;
378
- this.renderScheduled = true;
379
- queueMicrotask(() => {
380
- this.renderScheduled = false;
381
- if (this._running)
382
- this.draw();
383
- });
384
- }
385
- draw() {
386
- // [LAW:single-enforcer] One frame per tick. The autorun populates
387
- // `pendingFrame`; we consume it here. Fall back to a fresh compute if
388
- // draw is reached without a pending frame (test paths that call draw
389
- // outside the autorun, or the very first frame before autorun fired).
390
- const frame = this.pendingFrame ?? this.computeFrame();
391
- this.pendingFrame = null;
392
- const { width, lines, bounds, activeOverlays } = frame;
393
- // [LAW:single-enforcer] Bounds are written here, the only place that
394
- // computes layout. `bounds` is a plain field (see widget-base.ts) — no
395
- // MobX action required.
396
- for (const { widget, bounds: b } of bounds)
397
- widget.bounds = b;
398
- // [LAW:single-enforcer] Hit-test z-order is published by the same
399
- // pass that wrote the bounds. The `widgets` getter reads this set to
400
- // partition `widgetList` into [base, overlay-active] so EventRouter's
401
- // topmostHit sees overlay owners as topmost.
402
- this._activeOverlays = activeOverlays;
403
- const newCount = lines.length;
404
- const drawCount = Math.max(newCount, this.lastLineCount);
405
- // [LAW:types-are-the-program] After the previous frame wrote N lines
406
- // with N-1 newlines between them, the cursor sits at end-of-row-N (no
407
- // trailing newline). Rewinding to the top of the frame is N-1 rows up
408
- // plus a CR. `\x1b[0A` is still one row up on some terminals, so we
409
- // skip the CSI entirely when there's only one row to overwrite.
410
- let buf = "";
411
- if (this.lastLineCount > 1) {
412
- buf += `\x1b[${this.lastLineCount - 1}A`;
413
- }
414
- buf += "\r";
415
- for (let i = 0; i < drawCount; i++) {
416
- const line = lines[i];
417
- if (line) {
418
- // [LAW:single-enforcer] Clip each line to terminal width so wide
419
- // content can't soft-wrap and break the "1 frame row = 1 terminal
420
- // row" invariant the redraw loop depends on. Without this, a wrapped
421
- // line shifts every later row down by one terminal cell, which
422
- // misaligns overlays and leaves wrap-continuation residue when later
423
- // frames write narrower content over the same logical row.
424
- // Use the frame's captured width, not a re-read of this.width —
425
- // see FrameLayout for the rationale.
426
- const clipped = Segment.adjustLineLength(line, width, undefined, false);
427
- buf += segmentsToString(clipped, this.destination);
428
- }
429
- // [LAW:single-enforcer] Erase-to-end-of-line is the single mechanism
430
- // for overwriting stale content. We do not pre-clear lines.
431
- buf += "\x1b[K";
432
- if (i < drawCount - 1)
433
- buf += "\n";
434
- }
435
- this.host.write(buf);
436
- this.lastLineCount = drawCount;
437
- }
438
- };
439
- })();
440
- export { DefaultScreen };
441
- // Normalize a MountEntry into its internal { widget, placement } form.
442
- // Bare widgets default to flow placement so existing call sites (and tests)
443
- // that pass `mount(a, b, c)` keep working unchanged.
444
- function normalizeEntry(entry) {
445
- if ("widget" in entry && "placement" in entry) {
446
- validatePlacement(entry.placement);
447
- return { widget: entry.widget, placement: entry.placement };
448
- }
449
- return { widget: entry, placement: FLOW };
450
- }
451
- // [LAW:types-are-the-program] mount() is the trust boundary for placements.
452
- // `x` and `y` are typed as `number` but the layout pipeline assumes
453
- // non-negative integers — negative coordinates would index out of bounds in
454
- // paintLines and either crash on bang-asserts or silently skip rows. Reject
455
- // at construction so the rest of the pipeline can assume validity.
456
- function validatePlacement(p) {
457
- if (p.kind !== "fixed")
458
- return;
459
- if (!Number.isInteger(p.x) || !Number.isInteger(p.y) || p.x < 0 || p.y < 0) {
460
- throw new RangeError(`fixed Placement requires non-negative integer x and y; got (${p.x}, ${p.y})`);
461
- }
462
- }
463
- // Paint `widgetLines` into the frame `lines` at top-left (x, y). Grows the
464
- // frame as needed and uses cell-accurate splice semantics so a fixed
465
- // placement or overlay overwrites whatever was painted earlier underneath.
466
- //
467
- // [LAW:single-enforcer] All compositing into the frame buffer goes through
468
- // this function. flow/inline/fixed placements and the overlay pass all call
469
- // it, so the rules for "how characters land at (x, y)" live in one place.
470
- function paintLines(lines, widgetLines, x, y) {
471
- if (widgetLines.length === 0)
472
- return;
473
- while (lines.length < y + widgetLines.length)
474
- lines.push([]);
475
- for (let i = 0; i < widgetLines.length; i++) {
476
- const target = lines[y + i];
477
- const source = widgetLines[i];
478
- const sourceWidth = lineCellLength(source);
479
- if (sourceWidth === 0)
480
- continue;
481
- if (x === 0 && target.length === 0) {
482
- lines[y + i] = source.slice();
483
- continue;
484
- }
485
- lines[y + i] = spliceCells(target, asCellCol(x), sourceWidth, source);
486
- }
487
- }
488
- function lineCellLength(line) {
489
- let total = 0;
490
- for (const s of line)
491
- total += s.cellLength;
492
- return asCellCol(total);
493
- }
494
- // Return a new row with cells [start, start+length) replaced by
495
- // `replacement`. Pads the prefix with spaces if `row` is shorter than
496
- // `start`. Honors wide characters and styled segments by splitting at cell
497
- // boundaries.
498
- function spliceCells(row, start, length, replacement) {
499
- const rowWidth = lineCellLength(row);
500
- const padded = row.slice();
501
- if (rowWidth < start) {
502
- padded.push(new Segment(" ".repeat(start - rowWidth)));
503
- }
504
- const prefix = [];
505
- const suffix = [];
506
- let cursor = 0;
507
- for (const seg of padded) {
508
- const segEnd = cursor + seg.cellLength;
509
- if (segEnd <= start) {
510
- prefix.push(seg);
511
- }
512
- else if (cursor >= start + length) {
513
- suffix.push(seg);
514
- }
515
- else {
516
- if (cursor < start) {
517
- const [head] = seg.splitCells(asCellCol(start - cursor));
518
- if (head.hasText)
519
- prefix.push(head);
520
- }
521
- if (segEnd > start + length) {
522
- const [, tail] = seg.splitCells(asCellCol(start + length - cursor));
523
- if (tail.hasText)
524
- suffix.push(tail);
525
- }
526
- }
527
- cursor = segEnd;
528
- }
529
- return [...prefix, ...replacement, ...suffix];
530
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * Slider widget — numeric value within [min, max] with step increments.
3
- * [LAW:dataflow-not-control-flow] every render emits exactly `width`
4
- * track cells; the marker cell index is data, not a branch.
5
- * [LAW:one-type-per-behavior] shared infrastructure inherited from
6
- * WidgetBase.
7
- *
8
- * Visual:
9
- * track — `width` cells of "─" (ASCII fallback "-")
10
- * marker — "●" (ASCII fallback "*") at round((value - min) / range * (width - 1))
11
- * filled — cells left of and including the marker use primary fg
12
- * on the default terminal bg
13
- * unfilled — cells right of the marker use surface fg on the default
14
- * terminal bg (no explicit bgcolor — the unfilled track
15
- * should sit transparently against whatever the panel /
16
- * parent paints underneath)
17
- * focused — underline on the segments
18
- * disabled — dim
19
- */
20
- import { Segment } from "../core/segment.js";
21
- import type { RenderOptions } from "../core/protocol.js";
22
- import type { TerminalTheme } from "../core/color.js";
23
- import { WidgetBase } from "./widget-base.js";
24
- import type { KeyEvent, WidgetMouseEvent } from "./types.js";
25
- export interface SliderOptions {
26
- value?: number;
27
- min?: number;
28
- max?: number;
29
- step?: number;
30
- width?: number;
31
- id?: string;
32
- disabled?: boolean;
33
- theme?: TerminalTheme;
34
- }
35
- export declare class Slider extends WidgetBase {
36
- readonly id: string;
37
- readonly focusable = true;
38
- accessor value: number;
39
- accessor min: number;
40
- accessor max: number;
41
- accessor step: number;
42
- accessor width: number;
43
- private accessor _theme;
44
- private _dragging;
45
- constructor(options?: SliderOptions);
46
- setTheme(theme: TerminalTheme): void;
47
- handleKey(event: KeyEvent): void;
48
- handleMouse(event: WidgetMouseEvent): void;
49
- private setValue;
50
- private setValueFromMouseX;
51
- render(options: RenderOptions): Iterable<Segment>;
52
- measure(_options: RenderOptions): {
53
- minimum: number;
54
- maximum: number;
55
- };
56
- private resolvePalette;
57
- }