@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,622 +0,0 @@
1
- /**
2
- * EventRouter — parses raw stdin into KeyEvent / WidgetMouseEvent values
3
- * and dispatches them to widgets via the Screen's FocusManager and bounds.
4
- *
5
- * [LAW:single-enforcer] ANSI escape parsing lives only here — widgets never
6
- * see raw bytes. One parser, one dispatch surface.
7
- *
8
- * [LAW:dataflow-not-control-flow] The same parse pipeline runs for every
9
- * input chunk: append → consume one event at a time until the buffer can't
10
- * yield more. The byte's value, not a side-mode, decides which event is
11
- * emitted. There is no "are we mid-sequence" mode flag — the buffer head
12
- * carries all the state we need.
13
- *
14
- * Lone-ESC handling: a bare `\x1b` is ambiguous (escape key vs. start of a
15
- * CSI sequence). When the buffer is drained but ends with a lone ESC, the
16
- * router defers emission via setTimeout(0). The next chunk cancels the timer
17
- * if it extends the sequence. Tests can call `flush()` to drain synchronously.
18
- */
19
- import { KeyEvent } from "./types.js";
20
- // Single-byte → key name table for the trivial cases.
21
- const SINGLE_BYTE_KEYS = {
22
- 0x09: "tab",
23
- 0x0a: "enter",
24
- 0x0d: "enter",
25
- 0x1b: "escape",
26
- 0x08: "backspace",
27
- 0x7f: "backspace",
28
- 0x20: "space",
29
- };
30
- // Final-byte → key name for the simple `ESC[<X>` arrow / nav forms.
31
- const CSI_LETTER_KEYS = {
32
- A: "up",
33
- B: "down",
34
- C: "right",
35
- D: "left",
36
- H: "home",
37
- F: "end",
38
- Z: "tab", // shift+tab encodes as ESC[Z
39
- };
40
- // `ESC[<n>~` numeric forms for navigation / function keys.
41
- const CSI_TILDE_KEYS = {
42
- "1": "home",
43
- "2": "insert",
44
- "3": "delete",
45
- "4": "end",
46
- "5": "pageup",
47
- "6": "pagedown",
48
- "7": "home",
49
- "8": "end",
50
- "11": "f1",
51
- "12": "f2",
52
- "13": "f3",
53
- "14": "f4",
54
- "15": "f5",
55
- "17": "f6",
56
- "18": "f7",
57
- "19": "f8",
58
- "20": "f9",
59
- "21": "f10",
60
- "23": "f11",
61
- "24": "f12",
62
- };
63
- // SS3 form `ESC O X` for F1–F4 (and arrow keys on some terminals).
64
- const SS3_KEYS = {
65
- P: "f1",
66
- Q: "f2",
67
- R: "f3",
68
- S: "f4",
69
- A: "up",
70
- B: "down",
71
- C: "right",
72
- D: "left",
73
- H: "home",
74
- F: "end",
75
- };
76
- const ESC = 0x1b;
77
- const LBRACKET = 0x5b; // [
78
- const LANGLE = 0x3c; // <
79
- const O_BYTE = 0x4f; // O
80
- const M_UPPER = 0x4d; // M
81
- const M_LOWER = 0x6d; // m
82
- const MOUSE_TRACK_ON = "\x1b[?1006h\x1b[?1000h\x1b[?1003h";
83
- const MOUSE_TRACK_OFF = "\x1b[?1003l\x1b[?1000l\x1b[?1006l";
84
- // [LAW:single-enforcer] Shared encode/decode singletons. The parser
85
- // receives `Uint8Array | string` from the host and walks bytes through
86
- // its state machine; conversion happens at the two boundaries (string
87
- // input → bytes via TextEncoder; printable-byte run → JS string via
88
- // TextDecoder) and only there. Module-level instances keep the seam
89
- // allocation-free and runtime-agnostic — both APIs are universal across
90
- // node and browser.
91
- const UTF8_ENCODER = new TextEncoder();
92
- const UTF8_DECODER = new TextDecoder("utf-8");
93
- const ASCII_DECODER = new TextDecoder("ascii");
94
- const EMPTY_BYTES = new Uint8Array(0);
95
- // Concatenate two byte arrays into a fresh Uint8Array. Replaces
96
- // `Buffer.concat([...])` so the parser holds no node-specific globals.
97
- function concatBytes(a, b) {
98
- if (a.length === 0)
99
- return b;
100
- if (b.length === 0)
101
- return a;
102
- const merged = new Uint8Array(a.length + b.length);
103
- merged.set(a, 0);
104
- merged.set(b, a.length);
105
- return merged;
106
- }
107
- export class EventRouter {
108
- source;
109
- host;
110
- manageMouse;
111
- manageRawMode;
112
- // [LAW:types-are-the-program] The parse buffer is `Uint8Array`, not
113
- // `Buffer` — node's Buffer extends Uint8Array, so Buffer chunks from
114
- // process.stdin satisfy this type and the parser still works on node,
115
- // while a browser host can emit native Uint8Array without the runtime
116
- // depending on a Buffer global polyfill.
117
- buffer = EMPTY_BYTES;
118
- running = false;
119
- dataUnsubscribe;
120
- // [LAW:types-are-the-program] `setTimeout`/`clearTimeout` are universal
121
- // across node and browser; `setImmediate` is node-only. Same one-macrotask
122
- // semantics for our purpose (defer until current sync work + queued
123
- // microtasks drain), without forcing a `setImmediate` polyfill on the
124
- // browser host.
125
- escTimer;
126
- // [LAW:single-enforcer] Drag capture lives only here. A mouse_down's hit
127
- // widget is recorded; mouse_move/mouse_up between then and the next
128
- // mouse_up route to this widget unconditionally so dragging outside its
129
- // bounds still drives state (e.g. Slider's _dragging). Cleared on the
130
- // next mouse_up.
131
- capturedWidget = null;
132
- // [LAW:dataflow-not-control-flow] Ordered chain; the dispatcher walks it
133
- // in priority tiers and stops as soon as some participant calls
134
- // `event.stop()`. Insertion order is preserved within each tier.
135
- keyChain = [];
136
- mouseHandlers = new Set();
137
- constructor(options) {
138
- const { screen } = options;
139
- this.source = isWidgetSource(screen)
140
- ? screen
141
- : { focusManager: screen.focusManager, getWidgets: () => screen.widgets };
142
- this.host = options.host;
143
- // [LAW:one-source-of-truth] The host owns the "is this a real
144
- // terminal?" question. Both raw-mode and mouse-tracking defaults
145
- // derive from the same isTTY — no separate input/output skew.
146
- const isTTY = this.host.isTTY;
147
- this.manageRawMode = options.manageRawMode ?? isTTY;
148
- this.manageMouse = options.manageMouse ?? isTTY;
149
- // [LAW:single-enforcer] FocusManager owns Tab/Shift+Tab traversal —
150
- // register it as a normal-priority handler so it participates in the
151
- // chain. This MUST be the first registration so it lands ahead of any
152
- // user-added normal handlers; widgets that want to suppress traversal
153
- // call `event.stop()` from their own handleKey and FocusManager
154
- // never runs for that event.
155
- this.onKey((event) => this.source.focusManager.handleKey(event));
156
- }
157
- // --- Lifecycle ---
158
- start() {
159
- if (this.running)
160
- return;
161
- this.running = true;
162
- // [LAW:dataflow-not-control-flow] No `if (host.setRawMode)` capability
163
- // check — the host absorbs that. Same goes for resume(): subscribing
164
- // via host.onData triggers the underlying stream's flow on node, no-ops
165
- // on transports that don't have a paused mode.
166
- if (this.manageRawMode)
167
- this.host.setRawMode(true);
168
- if (this.manageMouse)
169
- this.host.write(MOUSE_TRACK_ON);
170
- this.dataUnsubscribe = this.host.onData((chunk) => this.feed(chunk));
171
- }
172
- stop() {
173
- if (this.running) {
174
- this.running = false;
175
- if (this.dataUnsubscribe) {
176
- this.dataUnsubscribe();
177
- this.dataUnsubscribe = undefined;
178
- }
179
- if (this.manageMouse)
180
- this.host.write(MOUSE_TRACK_OFF);
181
- if (this.manageRawMode)
182
- this.host.setRawMode(false);
183
- }
184
- // [LAW:one-source-of-truth] Per-session state belongs to one session.
185
- // stop() leaves the router in the same shape a fresh construction
186
- // would: empty parse buffer, no captured widget, no pending ESC timer.
187
- // All cleanup is outside the `running` guard because feed() can arm
188
- // escTimer (and grow buffer) even when running is false — without
189
- // those, a pre-start feed could leave a deferred escape firing into
190
- // the next session. Clearing already-empty state is a no-op so this
191
- // stays safe on repeated stop().
192
- if (this.escTimer) {
193
- clearTimeout(this.escTimer);
194
- this.escTimer = undefined;
195
- }
196
- this.buffer = EMPTY_BYTES;
197
- this.capturedWidget = null;
198
- }
199
- // --- External hooks ---
200
- onKey(handler, options) {
201
- const entry = {
202
- handler,
203
- priority: options?.priority ?? "normal",
204
- };
205
- this.keyChain.push(entry);
206
- return () => {
207
- const idx = this.keyChain.indexOf(entry);
208
- if (idx !== -1)
209
- this.keyChain.splice(idx, 1);
210
- };
211
- }
212
- onMouse(handler) {
213
- this.mouseHandlers.add(handler);
214
- return () => this.mouseHandlers.delete(handler);
215
- }
216
- // --- Test / advanced API ---
217
- /** Feed a chunk of bytes (or a string of bytes) into the parser. */
218
- feed(chunk) {
219
- const next = typeof chunk === "string" ? UTF8_ENCODER.encode(chunk) : chunk;
220
- this.buffer = concatBytes(this.buffer, next);
221
- if (this.escTimer) {
222
- clearTimeout(this.escTimer);
223
- this.escTimer = undefined;
224
- }
225
- this.drain();
226
- // [LAW:dataflow-not-control-flow] If a lone ESC remains, defer emission
227
- // until the next macrotask so a follow-up chunk can extend it. The
228
- // explicit `0` is structural deferral, not a throttle — it just lets
229
- // the current sync work + queued microtasks drain before flushing.
230
- if (this.buffer.length === 1 && this.buffer[0] === ESC) {
231
- this.escTimer = setTimeout(() => {
232
- this.escTimer = undefined;
233
- this.flush();
234
- }, 0);
235
- }
236
- }
237
- /** Force any pending lone ESC out of the buffer. */
238
- flush() {
239
- if (this.escTimer) {
240
- clearTimeout(this.escTimer);
241
- this.escTimer = undefined;
242
- }
243
- if (this.buffer.length > 0 && this.buffer[0] === ESC && this.buffer.length === 1) {
244
- this.buffer = this.buffer.subarray(1);
245
- this.dispatchKey(new KeyEvent({ key: "escape", character: "", shift: false, ctrl: false, meta: false }));
246
- }
247
- }
248
- // --- Internals ---
249
- drain() {
250
- while (this.buffer.length > 0) {
251
- const result = this.consumeOne(this.buffer);
252
- if (result.kind === "incomplete")
253
- return;
254
- this.buffer = this.buffer.subarray(result.bytes);
255
- if (result.kind === "key")
256
- this.dispatchKey(result.event);
257
- else if (result.kind === "mouse")
258
- this.dispatchMouse(result.event);
259
- // skip → drop bytes silently
260
- }
261
- }
262
- consumeOne(buf) {
263
- const b0 = buf[0];
264
- if (b0 === ESC)
265
- return this.consumeEscape(buf);
266
- // Ctrl+C
267
- if (b0 === 0x03) {
268
- return {
269
- kind: "key",
270
- bytes: 1,
271
- event: new KeyEvent({ key: "c", character: "", shift: false, ctrl: true, meta: false }),
272
- };
273
- }
274
- // Named single-byte keys (tab/enter/space/backspace).
275
- const named = SINGLE_BYTE_KEYS[b0];
276
- if (named && named !== "escape") {
277
- return {
278
- kind: "key",
279
- bytes: 1,
280
- event: new KeyEvent({
281
- key: named,
282
- character: named === "space" ? " " : "",
283
- shift: false,
284
- ctrl: false,
285
- meta: false,
286
- }),
287
- };
288
- }
289
- // Ctrl+letter (Ctrl+A=0x01 .. Ctrl+Z=0x1a), excluding the bytes already
290
- // claimed by SINGLE_BYTE_KEYS above (tab=0x09, enter=0x0a/0x0d, backspace=0x08).
291
- if (b0 < 0x20) {
292
- const letter = String.fromCharCode(b0 + 0x60); // 0x01 → 'a'
293
- return {
294
- kind: "key",
295
- bytes: 1,
296
- event: new KeyEvent({ key: letter, character: "", shift: false, ctrl: true, meta: false }),
297
- };
298
- }
299
- // Printable ASCII / start of a UTF-8 multibyte character.
300
- return this.consumePrintable(buf);
301
- }
302
- consumePrintable(buf) {
303
- const b0 = buf[0];
304
- let len = 1;
305
- if (b0 >= 0xc0 && b0 < 0xe0)
306
- len = 2;
307
- else if (b0 >= 0xe0 && b0 < 0xf0)
308
- len = 3;
309
- else if (b0 >= 0xf0)
310
- len = 4;
311
- if (buf.length < len)
312
- return { kind: "incomplete" };
313
- const character = UTF8_DECODER.decode(buf.subarray(0, len));
314
- return {
315
- kind: "key",
316
- bytes: len,
317
- event: new KeyEvent({
318
- key: character.toLowerCase(),
319
- character,
320
- shift: character.length === 1 && character !== character.toLowerCase(),
321
- ctrl: false,
322
- meta: false,
323
- }),
324
- };
325
- }
326
- consumeEscape(buf) {
327
- if (buf.length === 1)
328
- return { kind: "incomplete" };
329
- const b1 = buf[1];
330
- // ESC [ ... → CSI
331
- if (b1 === LBRACKET)
332
- return this.consumeCSI(buf);
333
- // ESC O X → SS3 (F1–F4 / arrows on some terms)
334
- if (b1 === O_BYTE)
335
- return this.consumeSS3(buf);
336
- // ESC ESC → escape key (some terminals double-send). Consume BOTH bytes
337
- // so the user sees one escape event, not two. Without this, the second
338
- // ESC remained in the buffer and produced a second escape via the
339
- // lone-ESC flush path.
340
- if (b1 === ESC) {
341
- return {
342
- kind: "key",
343
- bytes: 2,
344
- event: new KeyEvent({ key: "escape", character: "", shift: false, ctrl: false, meta: false }),
345
- };
346
- }
347
- // ESC <named> / ESC <printable> → Alt-modified key.
348
- // Terminals encode Alt+<key> as the literal byte preceded by ESC, so
349
- // `ESC b` is Alt+B, `ESC <0x7f>` is Alt+Backspace, `ESC <0x20>` is
350
- // Alt+Space, etc. The router surfaces this as `{ key, meta: true }` —
351
- // matching the shape used for Ctrl-modified keys, so handlers can
352
- // branch on `event.meta` without parsing escape sequences themselves.
353
- //
354
- // [LAW:one-source-of-truth] Named single-byte keys (space, tab,
355
- // enter, backspace) use the SAME canonical key names on the Alt path
356
- // as on the non-Alt path. Without this, `Alt+Space` decoded as
357
- // `key: " "` while a bare space decoded as `key: "space"`, so a
358
- // handler couldn't match Alt+Space via `event.key === "space" &&
359
- // event.meta`. Routing through SINGLE_BYTE_KEYS first preserves the
360
- // canonical naming.
361
- //
362
- // [LAW:one-source-of-truth] Modifier decoding for *all* navigation keys
363
- // lives in this file: CSI param-style modifiers (e.g. `ESC[1;3D` for
364
- // Alt+Left) flow through `decodeModifier`, and ESC-prefixed Alt forms
365
- // flow through this branch. Widgets read `event.meta` and never re-parse.
366
- const namedAlt = SINGLE_BYTE_KEYS[b1];
367
- if (namedAlt && namedAlt !== "escape") {
368
- return {
369
- kind: "key",
370
- bytes: 2,
371
- event: new KeyEvent({
372
- key: namedAlt,
373
- character: "",
374
- shift: false,
375
- ctrl: false,
376
- meta: true,
377
- }),
378
- };
379
- }
380
- if (b1 >= 0x20 && b1 <= 0x7e) {
381
- const ch = String.fromCharCode(b1);
382
- return {
383
- kind: "key",
384
- bytes: 2,
385
- event: new KeyEvent({
386
- key: ch.toLowerCase(),
387
- character: "",
388
- shift: ch !== ch.toLowerCase(),
389
- ctrl: false,
390
- meta: true,
391
- }),
392
- };
393
- }
394
- // Truly unrecognised follow-up byte → treat the ESC as a lone escape
395
- // and re-process from b1 on the next pass.
396
- return {
397
- kind: "key",
398
- bytes: 1,
399
- event: new KeyEvent({ key: "escape", character: "", shift: false, ctrl: false, meta: false }),
400
- };
401
- }
402
- consumeSS3(buf) {
403
- if (buf.length < 3)
404
- return { kind: "incomplete" };
405
- const final = String.fromCharCode(buf[2]);
406
- const name = SS3_KEYS[final];
407
- if (!name)
408
- return { kind: "skip", bytes: 3 };
409
- return {
410
- kind: "key",
411
- bytes: 3,
412
- event: new KeyEvent({ key: name, character: "", shift: false, ctrl: false, meta: false }),
413
- };
414
- }
415
- consumeCSI(buf) {
416
- // buf starts with ESC [
417
- const next = buf[2];
418
- if (next === undefined)
419
- return { kind: "incomplete" };
420
- // X10 mouse: ESC [ M b x y (3 raw bytes after M)
421
- if (next === M_UPPER) {
422
- if (buf.length < 6)
423
- return { kind: "incomplete" };
424
- const cb = buf[3] - 32;
425
- const col = buf[4] - 32 - 1;
426
- const row = buf[5] - 32 - 1;
427
- return { kind: "mouse", bytes: 6, event: decodeMouseFromCb(cb, col, row, true) };
428
- }
429
- // SGR mouse: ESC [ < cb ; x ; y M|m
430
- if (next === LANGLE) {
431
- // Find terminator M or m.
432
- for (let i = 3; i < buf.length; i++) {
433
- const b = buf[i];
434
- if (b === M_UPPER || b === M_LOWER) {
435
- const params = ASCII_DECODER.decode(buf.subarray(3, i));
436
- const parts = params.split(";");
437
- if (parts.length !== 3)
438
- return { kind: "skip", bytes: i + 1 };
439
- const cb = parseInt(parts[0], 10);
440
- const col = parseInt(parts[1], 10) - 1;
441
- const row = parseInt(parts[2], 10) - 1;
442
- if (Number.isNaN(cb) || Number.isNaN(col) || Number.isNaN(row)) {
443
- return { kind: "skip", bytes: i + 1 };
444
- }
445
- return {
446
- kind: "mouse",
447
- bytes: i + 1,
448
- event: decodeMouseFromCb(cb, col, row, b === M_UPPER),
449
- };
450
- }
451
- }
452
- return { kind: "incomplete" };
453
- }
454
- // Generic CSI: collect parameter / intermediate bytes until a final byte
455
- // in the range 0x40–0x7e is found.
456
- let params = "";
457
- for (let i = 2; i < buf.length; i++) {
458
- const b = buf[i];
459
- if (b >= 0x40 && b <= 0x7e) {
460
- const final = String.fromCharCode(b);
461
- return decodeCSI(params, final, i + 1);
462
- }
463
- params += String.fromCharCode(b);
464
- }
465
- return { kind: "incomplete" };
466
- }
467
- // --- Dispatch ---
468
- // [LAW:dataflow-not-control-flow] Three-stage walk; same shape every
469
- // dispatch, only the event's `stopped` flag short-circuits later stages.
470
- // The router holds NO key-specific policy — Tab semantics live entirely
471
- // in FocusManager (registered as a normal-priority handler at construction).
472
- dispatchKey(event) {
473
- // [LAW:one-source-of-truth] Snapshot the chain once at the start of
474
- // dispatch. A handler that unsubscribes itself (or another handler)
475
- // splices the live keyChain and would shift indices under the running
476
- // for-of loop, silently skipping the next handler. The snapshot
477
- // freezes the participant set for this dispatch; later registrations
478
- // join the chain for the next event, removals take effect after.
479
- const chain = [...this.keyChain];
480
- // Stage 1: high-priority handlers (global overrides like Ctrl+C).
481
- for (const entry of chain) {
482
- if (event.stopped)
483
- return;
484
- if (entry.priority === "high")
485
- entry.handler(event);
486
- }
487
- if (event.stopped)
488
- return;
489
- // Stage 2: the focused widget.
490
- this.source.focusManager.current?.handleKey(event);
491
- if (event.stopped)
492
- return;
493
- // Stage 3: normal-priority handlers (FocusManager's Tab traversal,
494
- // plus any app-registered fallbacks).
495
- for (const entry of chain) {
496
- if (event.stopped)
497
- return;
498
- if (entry.priority === "normal")
499
- entry.handler(event);
500
- }
501
- }
502
- dispatchMouse(event) {
503
- for (const handler of this.mouseHandlers)
504
- handler(event);
505
- const widgets = this.source.getWidgets();
506
- // Hover tracking: any widget whose hover state changed gets an update
507
- // via the canonical setHovered (on WidgetBase). One setter, no fallback.
508
- if (event.type === "mouse_move") {
509
- for (const w of widgets) {
510
- if (!w.visible)
511
- continue;
512
- const inside = w.containsPoint(event.x, event.y);
513
- if (inside !== w.hovered)
514
- w.setHovered(inside);
515
- }
516
- }
517
- // [LAW:dataflow-not-control-flow] Same pipeline every event; the
518
- // capture value and event type pick the target. mouse_down opens a
519
- // capture, mouse_up closes it, mouse_move in between routes to the
520
- // captured widget regardless of pointer position. Scroll events
521
- // bypass capture (they're not drag-scoped).
522
- const useCapture = this.capturedWidget !== null &&
523
- (event.type === "mouse_move" || event.type === "mouse_up");
524
- const target = useCapture
525
- ? this.capturedWidget
526
- : topmostHit(widgets, event.x, event.y);
527
- if (target)
528
- target.handleMouse(event);
529
- if (event.type === "mouse_down")
530
- this.capturedWidget = target;
531
- else if (event.type === "mouse_up")
532
- this.capturedWidget = null;
533
- }
534
- }
535
- // --- Helpers ---
536
- function isWidgetSource(value) {
537
- return (!!value &&
538
- typeof value === "object" &&
539
- "getWidgets" in value &&
540
- typeof value.getWidgets === "function");
541
- }
542
- function decodeMouseFromCb(cb, x, y, isPress) {
543
- const isMotion = (cb & 32) !== 0;
544
- const isScroll = (cb & 64) !== 0;
545
- const button = cb & 3;
546
- const shift = !!(cb & 4);
547
- const ctrl = !!(cb & 16); // bit 16 = ctrl in xterm encoding
548
- if (isScroll) {
549
- const type = button === 0 ? "scroll_up" : "scroll_down";
550
- return { type, x, y, button: 0, shift, ctrl };
551
- }
552
- if (isMotion) {
553
- return { type: "mouse_move", x, y, button, shift, ctrl };
554
- }
555
- return {
556
- type: isPress ? "mouse_down" : "mouse_up",
557
- x,
558
- y,
559
- button,
560
- shift,
561
- ctrl,
562
- };
563
- }
564
- function decodeCSI(params, final, bytes) {
565
- // Handle modifier suffixes like `1;2A` (shift+up). We split params on `;`.
566
- const parts = params.split(";");
567
- const first = parts[0] ?? "";
568
- const mod = parseInt(parts[1] ?? "", 10);
569
- const { shift, ctrl, meta } = decodeModifier(mod);
570
- if (final === "~") {
571
- const name = CSI_TILDE_KEYS[first];
572
- if (!name)
573
- return { kind: "skip", bytes };
574
- return {
575
- kind: "key",
576
- bytes,
577
- event: new KeyEvent({ key: name, character: "", shift, ctrl, meta }),
578
- };
579
- }
580
- const name = CSI_LETTER_KEYS[final];
581
- if (!name)
582
- return { kind: "skip", bytes };
583
- // Special case: ESC[Z is shift+tab regardless of explicit modifier.
584
- // [LAW:one-source-of-truth] `character` is the printable character the
585
- // key would produce, or "" for non-printable / modifier-only events.
586
- // The non-shifted tab path emits character: "" (Tab is named, not
587
- // printed) — keep shift+tab consistent. Distinguish via key === "tab"
588
- // + shift, not character.
589
- if (final === "Z") {
590
- return {
591
- kind: "key",
592
- bytes,
593
- event: new KeyEvent({ key: "tab", character: "", shift: true, ctrl: false, meta: false }),
594
- };
595
- }
596
- return {
597
- kind: "key",
598
- bytes,
599
- event: new KeyEvent({ key: name, character: "", shift, ctrl, meta }),
600
- };
601
- }
602
- function decodeModifier(mod) {
603
- if (!Number.isFinite(mod) || mod <= 1) {
604
- return { shift: false, ctrl: false, meta: false };
605
- }
606
- const bits = mod - 1;
607
- return {
608
- shift: (bits & 1) !== 0,
609
- meta: (bits & 2) !== 0,
610
- ctrl: (bits & 4) !== 0,
611
- };
612
- }
613
- function topmostHit(widgets, x, y) {
614
- for (let i = widgets.length - 1; i >= 0; i--) {
615
- const w = widgets[i];
616
- if (!w.visible)
617
- continue;
618
- if (w.containsPoint(x, y))
619
- return w;
620
- }
621
- return null;
622
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * FocusManager — flat focus cycling over registered widgets.
3
- * [LAW:one-source-of-truth] single authority for which widget has focus.
4
- * [LAW:dataflow-not-control-flow] focus transitions are observable state;
5
- * widgets react to focus/blur events, the manager never skips dispatch.
6
- */
7
- import type { InteractiveWidget, FocusManager, KeyEvent } from "./types.js";
8
- import type { Unsubscribe } from "../core/subscription.js";
9
- export declare class DefaultFocusManager implements FocusManager {
10
- accessor widgetList: InteractiveWidget[];
11
- accessor currentWidget: InteractiveWidget | null;
12
- private readonly changeHandlers;
13
- get current(): InteractiveWidget | null;
14
- get widgets(): readonly InteractiveWidget[];
15
- register(widget: InteractiveWidget): void;
16
- unregister(widget: InteractiveWidget): void;
17
- next(): void;
18
- prev(): void;
19
- focus(widget: InteractiveWidget): void;
20
- blur(): void;
21
- onChange(handler: (current: InteractiveWidget | null) => void): Unsubscribe;
22
- handleKey(event: KeyEvent): void;
23
- private focusableWidgets;
24
- private setFocus;
25
- private emitChange;
26
- }