@alchemy.run/sigil 0.0.0-alpha.1 → 0.0.0-alpha.11

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 (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,303 @@
1
+ import { ESC, pasteStart, pasteEnd } from "#/ansi/escapes.ts";
2
+
3
+ const escape = ESC;
4
+
5
+ export type InputEvent = string | { readonly paste: string };
6
+
7
+ type ParsedInput = {
8
+ readonly events: InputEvent[];
9
+ readonly pending: string;
10
+ };
11
+
12
+ type ParsedSequence =
13
+ | {
14
+ readonly sequence: string;
15
+ readonly nextIndex: number;
16
+ }
17
+ | "pending"
18
+ | undefined;
19
+
20
+ const isCsiParameterByte = (byte: number): boolean => {
21
+ return byte >= 0x30 && byte <= 0x3f;
22
+ };
23
+
24
+ const isCsiIntermediateByte = (byte: number): boolean => {
25
+ return byte >= 0x20 && byte <= 0x2f;
26
+ };
27
+
28
+ const isCsiFinalByte = (byte: number): boolean => {
29
+ return byte >= 0x40 && byte <= 0x7e;
30
+ };
31
+
32
+ const parseCsiSequence = (
33
+ input: string,
34
+ startIndex: number,
35
+ prefixLength: number,
36
+ ): ParsedSequence => {
37
+ const csiPayloadStart = startIndex + prefixLength + 1;
38
+ let index = csiPayloadStart;
39
+ for (; index < input.length; index++) {
40
+ const byte = input.codePointAt(index);
41
+ if (byte === undefined) {
42
+ return "pending";
43
+ }
44
+
45
+ // A new Escape or Ctrl+C cancels a truncated control sequence. Keep the
46
+ // cancelling byte for the next event, never insert the abandoned prefix.
47
+ if (byte === 0x1b || byte === 0x03) {
48
+ return { sequence: "", nextIndex: index };
49
+ }
50
+
51
+ if (isCsiParameterByte(byte) || isCsiIntermediateByte(byte)) {
52
+ continue;
53
+ }
54
+
55
+ // Preserve legacy terminal function-key sequences like ESC[[A and ESC[[5~.
56
+ if (byte === 0x5b && index === csiPayloadStart) {
57
+ continue;
58
+ }
59
+
60
+ if (isCsiFinalByte(byte)) {
61
+ return {
62
+ sequence: input.slice(startIndex, index + 1),
63
+ nextIndex: index + 1,
64
+ };
65
+ }
66
+
67
+ return;
68
+ }
69
+
70
+ return "pending";
71
+ };
72
+
73
+ const parseSs3Sequence = (
74
+ input: string,
75
+ startIndex: number,
76
+ prefixLength: number,
77
+ ): ParsedSequence => {
78
+ const nextIndex = startIndex + prefixLength + 2;
79
+ if (nextIndex > input.length) {
80
+ return "pending";
81
+ }
82
+
83
+ const finalByte = input.codePointAt(nextIndex - 1);
84
+ if (finalByte === undefined || !isCsiFinalByte(finalByte)) {
85
+ return;
86
+ }
87
+
88
+ return {
89
+ sequence: input.slice(startIndex, nextIndex),
90
+ nextIndex,
91
+ };
92
+ };
93
+
94
+ const parseControlSequence = (
95
+ input: string,
96
+ startIndex: number,
97
+ prefixLength: number,
98
+ ): ParsedSequence => {
99
+ const sequenceType = input[startIndex + prefixLength];
100
+ if (sequenceType === undefined) {
101
+ return "pending";
102
+ }
103
+
104
+ if (sequenceType === "[") {
105
+ return parseCsiSequence(input, startIndex, prefixLength);
106
+ }
107
+
108
+ // OSC, DCS and APC replies are single events, even across stdin chunks.
109
+ if (sequenceType === "]" || sequenceType === "P" || sequenceType === "_") {
110
+ for (let index = startIndex + prefixLength + 1; index < input.length; index++) {
111
+ if (
112
+ input[index] === "\u0003" ||
113
+ (input[index] === escape && index + 1 < input.length && input[index + 1] !== "\\")
114
+ ) {
115
+ return { sequence: "", nextIndex: index };
116
+ }
117
+ const bel = sequenceType === "]" && input[index] === "\u0007";
118
+ const st = input[index] === escape && input[index + 1] === "\\";
119
+ if (bel || st) {
120
+ const nextIndex = index + (st ? 2 : 1);
121
+ return { sequence: input.slice(startIndex, nextIndex), nextIndex };
122
+ }
123
+ }
124
+ return "pending";
125
+ }
126
+
127
+ if (sequenceType === "O") {
128
+ return parseSs3Sequence(input, startIndex, prefixLength);
129
+ }
130
+
131
+ return;
132
+ };
133
+
134
+ const parseEscapedCodePoint = (
135
+ input: string,
136
+ escapeIndex: number,
137
+ ): {
138
+ readonly sequence: string;
139
+ readonly nextIndex: number;
140
+ } => {
141
+ const nextCodePoint = input.codePointAt(escapeIndex + 1);
142
+ const nextCodePointLength = nextCodePoint !== undefined && nextCodePoint > 0xff_ff ? 2 : 1;
143
+ const nextIndex = escapeIndex + 1 + nextCodePointLength;
144
+
145
+ return {
146
+ sequence: input.slice(escapeIndex, nextIndex),
147
+ nextIndex,
148
+ };
149
+ };
150
+
151
+ type ParsedEscapeSequence =
152
+ | {
153
+ readonly sequence: string;
154
+ readonly nextIndex: number;
155
+ }
156
+ | "pending";
157
+
158
+ const parseEscapeSequence = (input: string, escapeIndex: number): ParsedEscapeSequence => {
159
+ if (escapeIndex === input.length - 1) {
160
+ return "pending";
161
+ }
162
+
163
+ const next = input[escapeIndex + 1]!;
164
+ if (next === escape) {
165
+ if (escapeIndex + 2 >= input.length) {
166
+ return "pending";
167
+ }
168
+
169
+ const doubleEscapeSequence = parseControlSequence(input, escapeIndex, 2);
170
+ if (doubleEscapeSequence === "pending") {
171
+ return "pending";
172
+ }
173
+
174
+ if (doubleEscapeSequence) {
175
+ return doubleEscapeSequence;
176
+ }
177
+
178
+ return {
179
+ sequence: input.slice(escapeIndex, escapeIndex + 2),
180
+ nextIndex: escapeIndex + 2,
181
+ };
182
+ }
183
+
184
+ const controlSequence = parseControlSequence(input, escapeIndex, 1);
185
+ if (controlSequence === "pending") {
186
+ return "pending";
187
+ }
188
+
189
+ if (controlSequence) {
190
+ return controlSequence;
191
+ }
192
+
193
+ return parseEscapedCodePoint(input, escapeIndex);
194
+ };
195
+
196
+ /**
197
+ Split a chunk of non-escape text so that backspace bytes (`0x7F` and `0x08`) become individual events. When a user holds the backspace key, the terminal sends repeated bytes in a single stdin chunk. Without splitting, `parseKeypress` receives the multi-byte string and fails to recognize it as a key event, corrupting the input state.
198
+
199
+ Other control characters like `\r` and `\t` are NOT split because they can legitimately appear inside pasted text.
200
+ */
201
+ const splitBackspaceBytes = (text: string, events: InputEvent[]): void => {
202
+ let textSegmentStart = 0;
203
+
204
+ for (let index = 0; index < text.length; index++) {
205
+ const character = text[index]!;
206
+ if (character === "\u007F" || character === "\u0008") {
207
+ if (index > textSegmentStart) {
208
+ events.push(text.slice(textSegmentStart, index));
209
+ }
210
+
211
+ events.push(character);
212
+ textSegmentStart = index + 1;
213
+ }
214
+ }
215
+
216
+ if (textSegmentStart < text.length) {
217
+ events.push(text.slice(textSegmentStart));
218
+ }
219
+ };
220
+
221
+ const parseKeypresses = (input: string): ParsedInput => {
222
+ const events: InputEvent[] = [];
223
+ let index = 0;
224
+ const pendingFrom = (pendingStartIndex: number): ParsedInput => ({
225
+ events,
226
+ pending: input.slice(pendingStartIndex),
227
+ });
228
+
229
+ while (index < input.length) {
230
+ const escapeIndex = input.indexOf(escape, index);
231
+ if (escapeIndex === -1) {
232
+ splitBackspaceBytes(input.slice(index), events);
233
+ return {
234
+ events,
235
+ pending: "",
236
+ };
237
+ }
238
+
239
+ if (escapeIndex > index) {
240
+ splitBackspaceBytes(input.slice(index, escapeIndex), events);
241
+ }
242
+
243
+ const parsedEscapeSequence = parseEscapeSequence(input, escapeIndex);
244
+ if (parsedEscapeSequence === "pending") {
245
+ return pendingFrom(escapeIndex);
246
+ }
247
+
248
+ if (parsedEscapeSequence.sequence === pasteStart) {
249
+ const afterStart = parsedEscapeSequence.nextIndex;
250
+ const endIndex = input.indexOf(pasteEnd, afterStart);
251
+ if (endIndex === -1) {
252
+ return pendingFrom(escapeIndex);
253
+ }
254
+
255
+ events.push({ paste: input.slice(afterStart, endIndex) });
256
+ index = endIndex + pasteEnd.length;
257
+ continue;
258
+ }
259
+
260
+ if (parsedEscapeSequence.sequence.length > 0) events.push(parsedEscapeSequence.sequence);
261
+ index = parsedEscapeSequence.nextIndex;
262
+ }
263
+
264
+ return {
265
+ events,
266
+ pending: "",
267
+ };
268
+ };
269
+
270
+ export type InputParser = {
271
+ push: (chunk: string) => InputEvent[];
272
+ hasPendingEscape: () => boolean;
273
+ flushPendingEscape: () => string | undefined;
274
+ reset: () => void;
275
+ };
276
+
277
+ export const createInputParser = (): InputParser => {
278
+ let pending = "";
279
+ // Only ambiguous Escape/Alt prefixes may time out. Once a control
280
+ // sequence starts, keep it intact until its terminator arrives.
281
+ const hasPendingEscape = () => pending.startsWith(escape) && !/^\u001B{1,2}[[\]P_]/.test(pending);
282
+
283
+ return {
284
+ push(chunk) {
285
+ const parsedInput = parseKeypresses(pending + chunk);
286
+ pending = parsedInput.pending;
287
+ return parsedInput.events;
288
+ },
289
+ hasPendingEscape,
290
+ flushPendingEscape() {
291
+ if (!hasPendingEscape()) {
292
+ return;
293
+ }
294
+
295
+ const pendingEscape = pending;
296
+ pending = "";
297
+ return pendingEscape;
298
+ },
299
+ reset() {
300
+ pending = "";
301
+ },
302
+ };
303
+ };
@@ -0,0 +1,9 @@
1
+ // Store all instances of Ink (instance.js) to ensure that consecutive render() calls
2
+ // use the same instance of Ink and don't create a new one
3
+ //
4
+ // This map has to be stored in a separate file, because render.js creates instances,
5
+ // but instance.js should delete itself from the map on unmount
6
+
7
+ import type { Ink } from "#/ink.tsx";
8
+
9
+ export const instances = new WeakMap<NodeJS.WritableStream, Ink>();
@@ -0,0 +1,3 @@
1
+ // See jsx-runtime.ts for why the names are enumerated.
2
+ export { Fragment, jsxDEV } from "react/jsx-dev-runtime";
3
+ export type { JSX } from "react/jsx-dev-runtime";
@@ -0,0 +1,6 @@
1
+ // `react/jsx-runtime` is CommonJS; a star re-export carries no static names
2
+ // through the bundler, so the automatic-runtime surface is spelled out.
3
+ export { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ // `jsxImportSource` looks up the `JSX` namespace on this module to type
5
+ // elements and props. Type-only, so the bundle is unaffected.
6
+ export type { JSX } from "react/jsx-runtime";
@@ -0,0 +1,185 @@
1
+ import { kittyQuery } from "#/ansi/escapes.ts";
2
+
3
+ // Kitty keyboard protocol flags.
4
+ // @see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
5
+ export const kittyFlags = {
6
+ disambiguateEscapeCodes: 1,
7
+ reportEventTypes: 2,
8
+ reportAlternateKeys: 4,
9
+ reportAllKeysAsEscapeCodes: 8,
10
+ reportAssociatedText: 16,
11
+ } as const;
12
+
13
+ // Valid flag names for the kitty keyboard protocol.
14
+ export type KittyFlagName = keyof typeof kittyFlags;
15
+
16
+ // Converts an array of flag names to the corresponding bitmask value.
17
+ export function resolveFlags(flags: KittyFlagName[]): number {
18
+ let result = 0;
19
+ for (const flag of flags) {
20
+ // eslint-disable-next-line no-bitwise
21
+ result |= kittyFlags[flag];
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ // Kitty keyboard modifier bits.
28
+ // These are used in the modifier parameter of CSI u sequences.
29
+ // Note: The actual modifier value is (modifiers - 1) as per the protocol.
30
+ export const kittyModifiers = {
31
+ shift: 1,
32
+ alt: 2,
33
+ ctrl: 4,
34
+ super: 8,
35
+ hyper: 16,
36
+ meta: 32,
37
+ capsLock: 64,
38
+ numLock: 128,
39
+ } as const;
40
+
41
+ // Options for configuring kitty keyboard protocol.
42
+ export type KittyKeyboardOptions = {
43
+ // Mode for kitty keyboard protocol support.
44
+ // - 'auto': Attempt to detect terminal support (default)
45
+ // - 'enabled': Force enable the protocol
46
+ // - 'disabled': Never enable the protocol
47
+ mode?: "auto" | "enabled" | "disabled";
48
+
49
+ // Protocol flags to request from the terminal.
50
+ // Pass an array of flag name strings.
51
+ //
52
+ // Available flags:
53
+ // - 'disambiguateEscapeCodes' - Disambiguate escape codes (default)
54
+ // - 'reportEventTypes' - Report key press, repeat, and release events
55
+ // - 'reportAlternateKeys' - Report alternate key encodings
56
+ // - 'reportAllKeysAsEscapeCodes' - Report all keys as escape codes
57
+ // - 'reportAssociatedText' - Report associated text with key events
58
+ flags?: KittyFlagName[];
59
+ };
60
+
61
+ const textEncoder = new TextEncoder();
62
+
63
+ const kittyQueryEscapeByte = 0x1b;
64
+ const kittyQueryOpenBracketByte = 0x5b;
65
+ const kittyQueryQuestionMarkByte = 0x3f;
66
+ const kittyQueryLetterByte = 0x75;
67
+ const zeroByte = 0x30;
68
+ const nineByte = 0x39;
69
+
70
+ type KittyQueryResponseMatch = { state: "complete"; endIndex: number } | { state: "partial" };
71
+
72
+ const isDigitByte = (byte: number): boolean => byte >= zeroByte && byte <= nineByte;
73
+
74
+ const matchKittyQueryResponse = (
75
+ buffer: number[],
76
+ startIndex: number,
77
+ ): KittyQueryResponseMatch | undefined => {
78
+ if (
79
+ buffer[startIndex] !== kittyQueryEscapeByte ||
80
+ buffer[startIndex + 1] !== kittyQueryOpenBracketByte ||
81
+ buffer[startIndex + 2] !== kittyQueryQuestionMarkByte
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ let index = startIndex + 3;
87
+ const digitsStartIndex = index;
88
+ while (index < buffer.length && isDigitByte(buffer[index]!)) {
89
+ index++;
90
+ }
91
+
92
+ if (index === digitsStartIndex) {
93
+ return;
94
+ }
95
+
96
+ if (index === buffer.length) {
97
+ return { state: "partial" };
98
+ }
99
+
100
+ if (buffer[index] === kittyQueryLetterByte) {
101
+ return { state: "complete", endIndex: index };
102
+ }
103
+
104
+ return;
105
+ };
106
+
107
+ const hasCompleteKittyQueryResponse = (buffer: number[]): boolean => {
108
+ for (let index = 0; index < buffer.length; index++) {
109
+ const match = matchKittyQueryResponse(buffer, index);
110
+ if (match?.state === "complete") {
111
+ return true;
112
+ }
113
+ }
114
+
115
+ return false;
116
+ };
117
+
118
+ const stripKittyQueryResponsesAndTrailingPartial = (buffer: number[]): number[] => {
119
+ const keptBytes: number[] = [];
120
+ let index = 0;
121
+ while (index < buffer.length) {
122
+ const match = matchKittyQueryResponse(buffer, index);
123
+ if (match?.state === "complete") {
124
+ index = match.endIndex + 1;
125
+ continue;
126
+ }
127
+
128
+ if (match?.state === "partial") {
129
+ break;
130
+ }
131
+
132
+ keptBytes.push(buffer[index]!);
133
+ index++;
134
+ }
135
+
136
+ return keptBytes;
137
+ };
138
+
139
+ // Query the terminal for kitty keyboard protocol support. The CSI ? u query is
140
+ // safe to send to any terminal — unsupporting terminals simply won't respond,
141
+ // and the 200ms timeout handles that. Stdin bytes that aren't part of the
142
+ // protocol response are re-emitted so they aren't lost from the caller's input
143
+ // pipeline. Returns a cancel function; cancelling (or the timeout firing)
144
+ // means `onSupported` never runs.
145
+ export const detectKittySupport = (
146
+ stdin: NodeJS.ReadableStream,
147
+ stdout: { write: (data: string) => unknown },
148
+ onSupported: () => void,
149
+ ): (() => void) => {
150
+ let responseBuffer: number[] = [];
151
+
152
+ const cleanup = (): void => {
153
+ clearTimeout(timer);
154
+ stdin.removeListener("data", onData);
155
+
156
+ // Re-emit any buffered data that wasn't the protocol response.
157
+ // Clear responseBuffer afterwards to make cleanup idempotent.
158
+ const remaining = stripKittyQueryResponsesAndTrailingPartial(responseBuffer);
159
+ responseBuffer = [];
160
+ if (remaining.length > 0) {
161
+ stdin.unshift(Uint8Array.from(remaining));
162
+ }
163
+ };
164
+
165
+ const onData = (data: Uint8Array | string): void => {
166
+ const chunk = typeof data === "string" ? textEncoder.encode(data) : data;
167
+ for (const byte of chunk) {
168
+ responseBuffer.push(byte);
169
+ }
170
+
171
+ if (hasCompleteKittyQueryResponse(responseBuffer)) {
172
+ cleanup();
173
+ onSupported();
174
+ }
175
+ };
176
+
177
+ // Attach listener before writing the query so that synchronous
178
+ // or immediate responses are not missed.
179
+ stdin.on("data", onData);
180
+ const timer = setTimeout(cleanup, 200);
181
+
182
+ stdout.write(kittyQuery);
183
+
184
+ return cleanup;
185
+ };
@@ -0,0 +1,62 @@
1
+ import { type DOMElement } from "#/dom.ts";
2
+
3
+ type Output = {
4
+ /**
5
+ Horizontal position (0-based column) within the live layout region.
6
+ */
7
+ x: number;
8
+
9
+ /**
10
+ Vertical position (0-based row) within the live layout region.
11
+ */
12
+ y: number;
13
+
14
+ /**
15
+ Element width.
16
+ */
17
+ width: number;
18
+
19
+ /**
20
+ Element height.
21
+ */
22
+ height: number;
23
+ };
24
+
25
+ /**
26
+ Measure the layout metrics of a particular `<Box>` element.
27
+ Returns an object with `x`, `y`, `width`, and `height` properties.
28
+
29
+ `x` and `y` are the element's position within the live layout region, computed by walking up the layout tree and accumulating each ancestor's offset. These are layout-tree coordinates, not terminal viewport coordinates. To compare them with mouse events, convert the event coordinates using the live region's viewport position. This is necessary even in alternate-screen mode when output, such as `<Static>` content, appears above the live region.
30
+
31
+ Note: `measureElement()` returns `{x: 0, y: 0, width: 0, height: 0}` when called during render (before layout is calculated). Call it from post-render code, such as `useEffect`, `useLayoutEffect`, input handlers, or timer callbacks. When content changes, pass the relevant dependency to your effect so it re-measures after each update.
32
+ */
33
+ export const measureElement = (node: DOMElement): Output => {
34
+ const { yogaNode } = node;
35
+
36
+ if (!yogaNode) {
37
+ return { x: 0, y: 0, width: 0, height: 0 };
38
+ }
39
+
40
+ let x = yogaNode.getComputedLeft();
41
+ let y = yogaNode.getComputedTop();
42
+
43
+ let current = node.parentNode;
44
+
45
+ while (current) {
46
+ if (current.yogaNode) {
47
+ x += current.yogaNode.getComputedLeft();
48
+ y += current.yogaNode.getComputedTop();
49
+ }
50
+
51
+ current = current.parentNode;
52
+ }
53
+
54
+ return {
55
+ x,
56
+ y,
57
+ width: yogaNode.getComputedWidth(),
58
+ height: yogaNode.getComputedHeight(),
59
+ };
60
+ };
61
+
62
+ export type { Output as ElementMetrics };
@@ -0,0 +1,31 @@
1
+ import { widestLine } from "#/ansi/string-width.ts";
2
+ import { QuickLru as QuickLRU } from "#/quick-lru.ts";
3
+
4
+ const cache = new QuickLRU<string, Output>({ maxSize: 4096 });
5
+
6
+ type Output = {
7
+ width: number;
8
+ height: number;
9
+ };
10
+
11
+ export const measureText = (text: string): Output => {
12
+ if (text.length === 0) {
13
+ return {
14
+ width: 0,
15
+ height: 0,
16
+ };
17
+ }
18
+
19
+ const cachedDimensions = cache.get(text);
20
+
21
+ if (cachedDimensions) {
22
+ return cachedDimensions;
23
+ }
24
+
25
+ const width = widestLine(text);
26
+ const height = text.split("\n").length;
27
+ const dimensions = { width, height };
28
+ cache.set(text, dimensions);
29
+
30
+ return dimensions;
31
+ };