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

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 (212) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +70 -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 +3209 -0
  27. package/dist/osc-BFKKSqpg.js +71 -0
  28. package/dist/osc-Cn0fw77g.d.ts +23 -0
  29. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  30. package/dist/query-BNc2B8GD.d.ts +152 -0
  31. package/dist/router.d.ts +392 -0
  32. package/dist/router.js +709 -0
  33. package/dist/sample-Cqw1bjUL.js +445 -0
  34. package/dist/screen-CgC2WlVM.d.ts +49 -0
  35. package/dist/screen-CiPytswf.js +342 -0
  36. package/dist/screen.d.ts +5 -0
  37. package/dist/screen.js +5 -0
  38. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  39. package/dist/serialize-BTkAZgw1.js +79 -0
  40. package/dist/session-DDQ5V300.js +723 -0
  41. package/dist/sgr-BhwaWAJB.js +246 -0
  42. package/dist/store-C1P5fOUi.d.ts +72 -0
  43. package/dist/string-width-CijQwpIk.js +69 -0
  44. package/dist/strip-BvU4toXG.js +6 -0
  45. package/dist/terminal.d.ts +121 -0
  46. package/dist/terminal.js +2 -0
  47. package/dist/tokenize-AjqbvtiT.js +1242 -0
  48. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  49. package/dist/truncate-D31fhU6i.js +562 -0
  50. package/dist/use-focus-CcCIdCWA.js +1337 -0
  51. package/dist/yoga-5jKhYCJC.js +3465 -0
  52. package/dist/yoga.d.ts +2 -0
  53. package/dist/yoga.js +2 -0
  54. package/package.json +70 -24
  55. package/src/ansi/chalk.ts +138 -0
  56. package/src/ansi/cursor.ts +46 -0
  57. package/src/ansi/east-asian-width.ts +259 -0
  58. package/src/ansi/escapes.ts +120 -0
  59. package/src/ansi/graphemes.ts +8 -0
  60. package/src/ansi/hyperlink.ts +44 -0
  61. package/src/ansi/index.ts +27 -0
  62. package/src/ansi/osc.ts +77 -0
  63. package/src/ansi/sgr.ts +234 -0
  64. package/src/ansi/slice.ts +43 -0
  65. package/src/ansi/string-width.ts +121 -0
  66. package/src/ansi/strip.ts +13 -0
  67. package/src/ansi/tokenize.ts +380 -0
  68. package/src/ansi/truncate.ts +196 -0
  69. package/src/ansi/wrap.ts +765 -0
  70. package/src/ansi-tokenizer.ts +510 -0
  71. package/src/capabilities/color-policy.ts +34 -0
  72. package/src/capabilities/detect.ts +608 -0
  73. package/src/capabilities/index.ts +37 -0
  74. package/src/capabilities/query.ts +679 -0
  75. package/src/capabilities/store.ts +394 -0
  76. package/src/code-excerpt.ts +39 -0
  77. package/src/color/index.ts +3 -0
  78. package/src/color/paint.ts +169 -0
  79. package/src/color/palette.ts +48 -0
  80. package/src/color/sample.ts +323 -0
  81. package/src/color.ts +1 -0
  82. package/src/components/AccessibilityContext.ts +5 -0
  83. package/src/components/AnimationContext.ts +24 -0
  84. package/src/components/AnsiText.tsx +42 -0
  85. package/src/components/App.tsx +878 -0
  86. package/src/components/AppContext.ts +111 -0
  87. package/src/components/BackgroundContext.ts +7 -0
  88. package/src/components/Box.tsx +100 -0
  89. package/src/components/CursorContext.ts +19 -0
  90. package/src/components/ErrorBoundary.tsx +39 -0
  91. package/src/components/ErrorOverview.tsx +135 -0
  92. package/src/components/FocusContext.ts +30 -0
  93. package/src/components/Hyperlink.tsx +56 -0
  94. package/src/components/Newline.tsx +16 -0
  95. package/src/components/Spacer.tsx +11 -0
  96. package/src/components/Static.tsx +60 -0
  97. package/src/components/StderrContext.ts +24 -0
  98. package/src/components/StdinContext.ts +48 -0
  99. package/src/components/StdoutContext.ts +26 -0
  100. package/src/components/TerminalOscContext.ts +25 -0
  101. package/src/components/Text.tsx +122 -0
  102. package/src/components/Transform.tsx +38 -0
  103. package/src/components/VirtualList.tsx +128 -0
  104. package/src/cursor-position.ts +103 -0
  105. package/src/devtools.ts +103 -0
  106. package/src/dom.ts +301 -0
  107. package/src/env.ts +12 -0
  108. package/src/get-max-width.ts +11 -0
  109. package/src/global.d.ts +38 -0
  110. package/src/glyphs.ts +99 -0
  111. package/src/hooks/use-animation.ts +142 -0
  112. package/src/hooks/use-app.ts +8 -0
  113. package/src/hooks/use-box-metrics.ts +134 -0
  114. package/src/hooks/use-capabilities.ts +73 -0
  115. package/src/hooks/use-cursor.ts +33 -0
  116. package/src/hooks/use-focus-manager.ts +62 -0
  117. package/src/hooks/use-focus.ts +82 -0
  118. package/src/hooks/use-input.ts +267 -0
  119. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  120. package/src/hooks/use-paste.ts +78 -0
  121. package/src/hooks/use-stderr.ts +8 -0
  122. package/src/hooks/use-stdin.ts +10 -0
  123. package/src/hooks/use-stdout.ts +8 -0
  124. package/src/hooks/use-terminal-osc.ts +59 -0
  125. package/src/hooks/use-virtual-scroll.ts +84 -0
  126. package/src/hooks/use-window-size.ts +37 -0
  127. package/src/index.ts +96 -0
  128. package/src/ink.tsx +1452 -0
  129. package/src/input-parser.ts +303 -0
  130. package/src/instances.ts +9 -0
  131. package/src/kitty-keyboard.ts +185 -0
  132. package/src/measure-element.ts +62 -0
  133. package/src/measure-text.ts +31 -0
  134. package/src/paint-tree.ts +220 -0
  135. package/src/parse-keypress.ts +515 -0
  136. package/src/parse-stack-line.ts +138 -0
  137. package/src/patch-console.ts +106 -0
  138. package/src/quick-lru.ts +85 -0
  139. package/src/reconciler.ts +425 -0
  140. package/src/render-background.ts +59 -0
  141. package/src/render-border.ts +167 -0
  142. package/src/render-frame.ts +83 -0
  143. package/src/render-to-string.ts +146 -0
  144. package/src/render.ts +284 -0
  145. package/src/router/components.tsx +343 -0
  146. package/src/router/context.ts +41 -0
  147. package/src/router/history.ts +194 -0
  148. package/src/router/hooks.tsx +391 -0
  149. package/src/router/index.ts +34 -0
  150. package/src/router/matcher.ts +571 -0
  151. package/src/sanitize-ansi.ts +33 -0
  152. package/src/screen/ansi.ts +184 -0
  153. package/src/screen/canvas.ts +160 -0
  154. package/src/screen/cell.ts +138 -0
  155. package/src/screen/color-profile.ts +47 -0
  156. package/src/screen/geometry.ts +9 -0
  157. package/src/screen/index.ts +6 -0
  158. package/src/screen/screen.ts +305 -0
  159. package/src/screen/serialize.ts +129 -0
  160. package/src/screen.ts +1 -0
  161. package/src/semantic-text-style.ts +118 -0
  162. package/src/signal-exit.ts +106 -0
  163. package/src/squash-text-nodes.ts +37 -0
  164. package/src/stream.ts +28 -0
  165. package/src/structured-text.ts +325 -0
  166. package/src/styles.ts +753 -0
  167. package/src/terminal/index.ts +2 -0
  168. package/src/terminal/inline-presenter.ts +120 -0
  169. package/src/terminal/input.ts +92 -0
  170. package/src/terminal/render-scheduler.ts +37 -0
  171. package/src/terminal/screen-presenter.ts +242 -0
  172. package/src/terminal/session.ts +408 -0
  173. package/src/terminal-size.ts +58 -0
  174. package/src/terminal.ts +1 -0
  175. package/src/testing/browser.ts +588 -0
  176. package/src/testing/emulators.ts +205 -0
  177. package/src/testing/explorer-app/index.html +12 -0
  178. package/src/testing/explorer-app/main.ts +381 -0
  179. package/src/testing/explorer-app/style.css +194 -0
  180. package/src/testing/explorer-app/tsconfig.json +15 -0
  181. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  182. package/src/testing/index.ts +26 -0
  183. package/src/testing/keys.ts +56 -0
  184. package/src/testing/live.ts +85 -0
  185. package/src/testing/matchers.ts +70 -0
  186. package/src/testing/public.ts +94 -0
  187. package/src/testing/terminal.ts +361 -0
  188. package/src/testing/vitest.ts +157 -0
  189. package/src/throttle.ts +73 -0
  190. package/src/transform-adapter.ts +14 -0
  191. package/src/types.ts +10 -0
  192. package/src/virtual-scroll.ts +133 -0
  193. package/src/wrap-text.ts +54 -0
  194. package/src/yoga/config.ts +57 -0
  195. package/src/yoga/core/absoluteLayout.ts +626 -0
  196. package/src/yoga/core/baseline.ts +66 -0
  197. package/src/yoga/core/cache.ts +136 -0
  198. package/src/yoga/core/calculateLayout.ts +2926 -0
  199. package/src/yoga/core/config.ts +104 -0
  200. package/src/yoga/core/flexLine.ts +177 -0
  201. package/src/yoga/core/helpers.ts +293 -0
  202. package/src/yoga/core/layoutResults.ts +167 -0
  203. package/src/yoga/core/node.ts +611 -0
  204. package/src/yoga/core/numeric.ts +44 -0
  205. package/src/yoga/core/pixelGrid.ts +151 -0
  206. package/src/yoga/core/style.ts +887 -0
  207. package/src/yoga/core/types.ts +224 -0
  208. package/src/yoga/generated/YGEnums.ts +263 -0
  209. package/src/yoga/index.ts +19 -0
  210. package/src/yoga/node.ts +1140 -0
  211. package/dist/index.d.mts +0 -2379
  212. package/dist/index.mjs +0 -10072
package/src/dom.ts ADDED
@@ -0,0 +1,301 @@
1
+ import { measureText } from "#/measure-text.ts";
2
+ import type { SemanticTextStyle } from "#/semantic-text-style.ts";
3
+ import { squashTextNodes } from "#/squash-text-nodes.ts";
4
+ import { type Styles } from "#/styles.ts";
5
+ import { type AnsiTransformer } from "#/transform-adapter.ts";
6
+ import { wrapText } from "#/wrap-text.ts";
7
+ import { Yoga, type Node as YogaNode } from "#/yoga/index.ts";
8
+
9
+ type InkNode = {
10
+ parentNode: DOMElement | undefined;
11
+ yogaNode?: YogaNode;
12
+ internal_static?: boolean;
13
+ style: Styles;
14
+ };
15
+
16
+ type LayoutListener = () => void;
17
+
18
+ export type TextName = "#text";
19
+ export type ElementNames = "ink-root" | "ink-box" | "ink-text" | "ink-virtual-text";
20
+
21
+ export type NodeNames = ElementNames | TextName;
22
+
23
+ // eslint-disable-next-line @typescript-eslint/naming-convention
24
+ export type DOMElement = {
25
+ nodeName: ElementNames;
26
+ attributes: Record<string, DOMNodeAttribute>;
27
+ childNodes: DOMNode[];
28
+ internal_ansi?: boolean;
29
+ internal_transform?: AnsiTransformer;
30
+ internal_textStyle?: SemanticTextStyle;
31
+
32
+ internal_accessibility?: {
33
+ role?:
34
+ | "button"
35
+ | "checkbox"
36
+ | "combobox"
37
+ | "list"
38
+ | "listbox"
39
+ | "listitem"
40
+ | "menu"
41
+ | "menuitem"
42
+ | "option"
43
+ | "progressbar"
44
+ | "radio"
45
+ | "radiogroup"
46
+ | "tab"
47
+ | "tablist"
48
+ | "table"
49
+ | "textbox"
50
+ | "timer"
51
+ | "toolbar";
52
+ state?: {
53
+ busy?: boolean;
54
+ checked?: boolean;
55
+ disabled?: boolean;
56
+ expanded?: boolean;
57
+ multiline?: boolean;
58
+ multiselectable?: boolean;
59
+ readonly?: boolean;
60
+ required?: boolean;
61
+ selected?: boolean;
62
+ };
63
+ };
64
+
65
+ // Internal properties
66
+ isStaticDirty?: boolean;
67
+ staticNode?: DOMElement;
68
+ // Tracks the previous commit's `staticNode` so the reconciler can detect identity changes (mount, unmount, key-driven remount) and reset `fullStaticOutput`.
69
+ previousStaticNode?: DOMElement;
70
+ onComputeLayout?: () => void;
71
+ onRender?: () => void;
72
+ onImmediateRender?: () => void;
73
+ onStaticChange?: () => void;
74
+ internal_layoutListeners?: Set<LayoutListener>;
75
+ } & InkNode;
76
+
77
+ export type TextNode = {
78
+ nodeName: TextName;
79
+ nodeValue: string;
80
+ } & InkNode;
81
+
82
+ // eslint-disable-next-line @typescript-eslint/naming-convention
83
+ export type DOMNode<T = { nodeName: NodeNames }> = T extends {
84
+ nodeName: infer U;
85
+ }
86
+ ? U extends "#text"
87
+ ? TextNode
88
+ : DOMElement
89
+ : never;
90
+
91
+ // eslint-disable-next-line @typescript-eslint/naming-convention
92
+ export type DOMNodeAttribute = boolean | string | number;
93
+
94
+ export const createNode = (nodeName: ElementNames): DOMElement => {
95
+ const node: DOMElement = {
96
+ nodeName,
97
+ style: {},
98
+ attributes: {},
99
+ childNodes: [],
100
+ parentNode: undefined,
101
+ yogaNode: nodeName === "ink-virtual-text" ? undefined : Yoga.Node.create(),
102
+ // eslint-disable-next-line @typescript-eslint/naming-convention
103
+ internal_accessibility: {},
104
+ };
105
+
106
+ if (nodeName === "ink-text") {
107
+ node.yogaNode?.setMeasureFunc((width) => measureTextNode(node, width));
108
+ }
109
+
110
+ return node;
111
+ };
112
+
113
+ export const appendChildNode = (node: DOMElement, childNode: DOMElement): void => {
114
+ if (childNode.parentNode) {
115
+ removeChildNode(childNode.parentNode, childNode);
116
+ }
117
+
118
+ childNode.parentNode = node;
119
+ node.childNodes.push(childNode);
120
+
121
+ if (childNode.yogaNode) {
122
+ node.yogaNode?.insertChild(childNode.yogaNode, node.yogaNode.getChildCount());
123
+ }
124
+
125
+ if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
126
+ markNodeAsDirty(node);
127
+ }
128
+ };
129
+
130
+ export const insertBeforeNode = (
131
+ node: DOMElement,
132
+ newChildNode: DOMNode,
133
+ beforeChildNode: DOMNode,
134
+ ): void => {
135
+ if (newChildNode.parentNode) {
136
+ removeChildNode(newChildNode.parentNode, newChildNode);
137
+ }
138
+
139
+ newChildNode.parentNode = node;
140
+
141
+ const index = node.childNodes.indexOf(beforeChildNode);
142
+ if (index >= 0) {
143
+ node.childNodes.splice(index, 0, newChildNode);
144
+ if (newChildNode.yogaNode) {
145
+ node.yogaNode?.insertChild(newChildNode.yogaNode, index);
146
+ }
147
+ } else {
148
+ node.childNodes.push(newChildNode);
149
+
150
+ if (newChildNode.yogaNode) {
151
+ node.yogaNode?.insertChild(newChildNode.yogaNode, node.yogaNode.getChildCount());
152
+ }
153
+ }
154
+
155
+ if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
156
+ markNodeAsDirty(node);
157
+ }
158
+ };
159
+
160
+ export const removeChildNode = (node: DOMElement, removeNode: DOMNode): void => {
161
+ if (removeNode.yogaNode) {
162
+ removeNode.parentNode?.yogaNode?.removeChild(removeNode.yogaNode);
163
+ }
164
+
165
+ removeNode.parentNode = undefined;
166
+
167
+ const index = node.childNodes.indexOf(removeNode);
168
+ if (index >= 0) {
169
+ node.childNodes.splice(index, 1);
170
+ }
171
+
172
+ if (node.nodeName === "ink-text" || node.nodeName === "ink-virtual-text") {
173
+ markNodeAsDirty(node);
174
+ }
175
+ };
176
+
177
+ const nullifyYogaNodes = (node: DOMNode): void => {
178
+ node.yogaNode = undefined;
179
+
180
+ if (node.nodeName !== "#text") {
181
+ for (const childNode of node.childNodes) {
182
+ nullifyYogaNodes(childNode);
183
+ }
184
+ }
185
+ };
186
+
187
+ /**
188
+ Detach a removed subtree from the layout engine: drop the measure callback
189
+ and null the `yogaNode` reference on every DOM node within it.
190
+
191
+ Nulling the references makes every `?.yogaNode` guard in the codebase
192
+ effective for removed nodes, turns lingering access into a safe no-op (see
193
+ QwenLM/qwen-code#6820), and lets the garbage collector reclaim the Yoga
194
+ tree along with its closures.
195
+ */
196
+ export const detachYogaSubtree = (removeNode: DOMNode): void => {
197
+ removeNode.yogaNode?.unsetMeasureFunc();
198
+ nullifyYogaNodes(removeNode);
199
+ };
200
+
201
+ export const setAttribute = (node: DOMElement, key: string, value: DOMNodeAttribute): void => {
202
+ if (key === "internal_accessibility") {
203
+ node.internal_accessibility = value as DOMElement["internal_accessibility"];
204
+ return;
205
+ }
206
+
207
+ node.attributes[key] = value;
208
+ };
209
+
210
+ export const setStyle = (node: DOMNode, style?: Styles): void => {
211
+ // Rendering code assumes style is always an object.
212
+ node.style = style ?? {};
213
+ };
214
+
215
+ export const createTextNode = (text: string): TextNode => {
216
+ const node: TextNode = {
217
+ nodeName: "#text",
218
+ nodeValue: text,
219
+ yogaNode: undefined,
220
+ parentNode: undefined,
221
+ style: {},
222
+ };
223
+
224
+ setTextNodeValue(node, text);
225
+
226
+ return node;
227
+ };
228
+
229
+ const measureTextNode = function (node: DOMNode, width: number): { width: number; height: number } {
230
+ const text = node.nodeName === "#text" ? node.nodeValue : squashTextNodes(node);
231
+
232
+ const dimensions = measureText(text);
233
+
234
+ // Text fits into container, no need to wrap
235
+ if (dimensions.width <= width) {
236
+ return dimensions;
237
+ }
238
+
239
+ // This is happening when <Box> is shrinking child nodes and Yoga asks
240
+ // if we can fit this text node in a <1px space, so we just tell Yoga "no"
241
+ if (dimensions.width >= 1 && width > 0 && width < 1) {
242
+ return dimensions;
243
+ }
244
+
245
+ const textWrap = node.style?.textWrap ?? "wrap";
246
+
247
+ // `none` keeps the intrinsic dimensions and overflows the container.
248
+ if (textWrap === "none") {
249
+ return dimensions;
250
+ }
251
+
252
+ const wrappedText = wrapText(text, width, textWrap);
253
+
254
+ return measureText(wrappedText);
255
+ };
256
+
257
+ const findClosestYogaNode = (node?: DOMNode): YogaNode | undefined => {
258
+ if (!node?.parentNode) {
259
+ return;
260
+ }
261
+
262
+ return node.yogaNode ?? findClosestYogaNode(node.parentNode);
263
+ };
264
+
265
+ const markNodeAsDirty = (node?: DOMNode): void => {
266
+ // Mark closest Yoga node as dirty to measure text dimensions again
267
+ const yogaNode = findClosestYogaNode(node);
268
+ yogaNode?.markDirty();
269
+ };
270
+
271
+ export const setTextNodeValue = (node: TextNode, text: string): void => {
272
+ if (typeof text !== "string") {
273
+ text = String(text);
274
+ }
275
+
276
+ node.nodeValue = text;
277
+ markNodeAsDirty(node);
278
+ };
279
+
280
+ export const addLayoutListener = (rootNode: DOMElement, listener: LayoutListener): (() => void) => {
281
+ if (rootNode.nodeName !== "ink-root") {
282
+ return () => {};
283
+ }
284
+
285
+ rootNode.internal_layoutListeners ??= new Set();
286
+ rootNode.internal_layoutListeners.add(listener);
287
+
288
+ return () => {
289
+ rootNode.internal_layoutListeners?.delete(listener);
290
+ };
291
+ };
292
+
293
+ export const emitLayoutListeners = (rootNode: DOMElement): void => {
294
+ if (rootNode.nodeName !== "ink-root" || !rootNode.internal_layoutListeners) {
295
+ return;
296
+ }
297
+
298
+ for (const listener of rootNode.internal_layoutListeners) {
299
+ listener();
300
+ }
301
+ };
package/src/env.ts ADDED
@@ -0,0 +1,12 @@
1
+ // Every modern CI provider sets CI=1 (or CI=true), so any non-empty value
2
+ export const isInCi = Boolean(process.env.CI);
3
+
4
+ export const isSigilDev = process.env.SIGIL_DEV === "true";
5
+
6
+ export const isScreenReader = process.env.SIGIL_SCREEN_READER === "true";
7
+
8
+ export const isWindows = process.platform === "win32";
9
+
10
+ export const isMacos = process.platform === "darwin";
11
+
12
+ export const isTty = (stream: NodeJS.ReadableStream) => "isTTY" in stream && stream.isTTY === true;
@@ -0,0 +1,11 @@
1
+ import { Yoga, type Node as YogaNode } from "#/yoga/index.ts";
2
+
3
+ export const getMaxWidth = (yogaNode: YogaNode) => {
4
+ return (
5
+ yogaNode.getComputedWidth() -
6
+ yogaNode.getComputedPadding(Yoga.EDGE_LEFT) -
7
+ yogaNode.getComputedPadding(Yoga.EDGE_RIGHT) -
8
+ yogaNode.getComputedBorder(Yoga.EDGE_LEFT) -
9
+ yogaNode.getComputedBorder(Yoga.EDGE_RIGHT)
10
+ );
11
+ };
@@ -0,0 +1,38 @@
1
+ import { type ReactNode, type Key, type Ref } from "react";
2
+
3
+ import { type DOMElement } from "#/dom.ts";
4
+ import type { SemanticTextStyle } from "#/semantic-text-style.ts";
5
+ import { type Styles } from "#/styles.ts";
6
+
7
+ declare module "react" {
8
+ namespace JSX {
9
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
10
+ interface IntrinsicElements {
11
+ "ink-box": Ink.Box;
12
+ "ink-text": Ink.Text;
13
+ }
14
+ }
15
+ }
16
+
17
+ declare namespace Ink {
18
+ type Box = {
19
+ internal_static?: boolean;
20
+ children?: ReactNode;
21
+ key?: Key;
22
+ ref?: Ref<DOMElement>;
23
+ style?: Omit<Styles, "textWrap">;
24
+ internal_accessibility?: DOMElement["internal_accessibility"];
25
+ };
26
+
27
+ type Text = {
28
+ children?: ReactNode;
29
+ key?: Key;
30
+ style?: Styles;
31
+
32
+ // eslint-disable-next-line @typescript-eslint/naming-convention
33
+ internal_ansi?: boolean;
34
+ internal_transform?: (children: string, index: number) => string;
35
+ internal_textStyle?: SemanticTextStyle;
36
+ internal_accessibility?: DOMElement["internal_accessibility"];
37
+ };
38
+ }
package/src/glyphs.ts ADDED
@@ -0,0 +1,99 @@
1
+ // Derived from `cli-boxes` (MIT, Sindre Sorhus).
2
+ export const BOXES = {
3
+ single: {
4
+ topLeft: "┌",
5
+ top: "─",
6
+ topRight: "┐",
7
+ right: "│",
8
+ bottomRight: "┘",
9
+ bottom: "─",
10
+ bottomLeft: "└",
11
+ left: "│",
12
+ },
13
+ double: {
14
+ topLeft: "╔",
15
+ top: "═",
16
+ topRight: "╗",
17
+ right: "║",
18
+ bottomRight: "╝",
19
+ bottom: "═",
20
+ bottomLeft: "╚",
21
+ left: "║",
22
+ },
23
+ round: {
24
+ topLeft: "╭",
25
+ top: "─",
26
+ topRight: "╮",
27
+ right: "│",
28
+ bottomRight: "╯",
29
+ bottom: "─",
30
+ bottomLeft: "╰",
31
+ left: "│",
32
+ },
33
+ bold: {
34
+ topLeft: "┏",
35
+ top: "━",
36
+ topRight: "┓",
37
+ right: "┃",
38
+ bottomRight: "┛",
39
+ bottom: "━",
40
+ bottomLeft: "┗",
41
+ left: "┃",
42
+ },
43
+ singleDouble: {
44
+ topLeft: "╓",
45
+ top: "─",
46
+ topRight: "╖",
47
+ right: "║",
48
+ bottomRight: "╜",
49
+ bottom: "─",
50
+ bottomLeft: "╙",
51
+ left: "║",
52
+ },
53
+ doubleSingle: {
54
+ topLeft: "╒",
55
+ top: "═",
56
+ topRight: "╕",
57
+ right: "│",
58
+ bottomRight: "╛",
59
+ bottom: "═",
60
+ bottomLeft: "╘",
61
+ left: "│",
62
+ },
63
+ classic: {
64
+ topLeft: "+",
65
+ top: "-",
66
+ topRight: "+",
67
+ right: "|",
68
+ bottomRight: "+",
69
+ bottom: "-",
70
+ bottomLeft: "+",
71
+ left: "|",
72
+ },
73
+ arrow: {
74
+ topLeft: "↘",
75
+ top: "↓",
76
+ topRight: "↙",
77
+ right: "←",
78
+ bottomRight: "↖",
79
+ bottom: "↑",
80
+ bottomLeft: "↗",
81
+ left: "→",
82
+ },
83
+ } as const;
84
+
85
+ export type BoxStyle = keyof typeof BOXES;
86
+
87
+ /**
88
+ The set of glyphs making up a box border, for custom `borderStyle` objects.
89
+ */
90
+ export type BoxGlyphs = {
91
+ readonly topLeft: string;
92
+ readonly top: string;
93
+ readonly topRight: string;
94
+ readonly right: string;
95
+ readonly bottomRight: string;
96
+ readonly bottom: string;
97
+ readonly bottomLeft: string;
98
+ readonly left: string;
99
+ };
@@ -0,0 +1,142 @@
1
+ import { useState, useLayoutEffect, useRef, useCallback, useContext } from "react";
2
+
3
+ import { animationContext as AnimationContext } from "#/components/AnimationContext.ts";
4
+
5
+ const defaultAnimationInterval = 100;
6
+ const maximumTimerInterval = 2_147_483_647;
7
+ const zeroAnimState = { frame: 0, time: 0, delta: 0 };
8
+
9
+ type Options = {
10
+ /**
11
+ Time between ticks in milliseconds.
12
+
13
+ @default 100
14
+ */
15
+ readonly interval?: number;
16
+
17
+ /**
18
+ Whether the animation is running. When set to `false`, the animation stops. When toggled back to `true`, all values reset to `0`.
19
+
20
+ @default true
21
+ */
22
+ readonly isActive?: boolean;
23
+ };
24
+
25
+ export type AnimationResult = {
26
+ /**
27
+ Discrete counter that increments by 1 each interval. Useful for indexed sequences like spinner frames.
28
+ */
29
+ readonly frame: number;
30
+
31
+ /**
32
+ Total elapsed time in milliseconds since the animation started or was last reset. Useful for continuous math-based animations like sine waves: `Math.sin(time / 1000 * Math.PI * 2)`.
33
+ */
34
+ readonly time: number;
35
+
36
+ /**
37
+ Time in milliseconds since the previous rendered tick. Accounts for throttled renders. Useful for physics-based or velocity-driven motion: `position += speed * delta`.
38
+ */
39
+ readonly delta: number;
40
+
41
+ /**
42
+ Resets `frame`, `time`, and `delta` to `0` and restarts timing from the current moment. Useful for one-shot animations triggered by events.
43
+ */
44
+ readonly reset: () => void;
45
+ };
46
+
47
+ /**
48
+ A React hook that drives animations. Returns a frame counter, elapsed time, frame delta, and a reset function. All animations share a single timer internally, so multiple animated components consolidate into one render cycle.
49
+
50
+ @example
51
+ ```
52
+ import {Text, useAnimation} from 'ink';
53
+
54
+ const Spinner = () => {
55
+ const {frame} = useAnimation({interval: 80});
56
+ const characters = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
57
+
58
+ return <Text>{characters[frame % characters.length]}</Text>;
59
+ };
60
+ ```
61
+ */
62
+ export function useAnimation(options?: Options): AnimationResult {
63
+ const { interval = defaultAnimationInterval, isActive = true } = options ?? {};
64
+ const safeInterval = normalizeAnimationInterval(interval);
65
+ const { subscribe, renderThrottleMs } = useContext(AnimationContext);
66
+ const [resetKey, setResetKey] = useState(0);
67
+ const [animState, setAnimState] = useState(zeroAnimState);
68
+ const nextRenderTimeRef = useRef(0);
69
+ const lastRenderTimeRef = useRef(0);
70
+ const previousOptionsRef = useRef({ isActive, safeInterval, resetKey });
71
+ const previousOptions = previousOptionsRef.current;
72
+ const shouldReset =
73
+ isActive &&
74
+ (safeInterval !== previousOptions.safeInterval ||
75
+ !previousOptions.isActive ||
76
+ resetKey !== previousOptions.resetKey);
77
+
78
+ const reset = useCallback(() => {
79
+ setResetKey((k) => k + 1);
80
+ }, []);
81
+
82
+ useLayoutEffect(() => {
83
+ if (!isActive) {
84
+ return;
85
+ }
86
+
87
+ // Reset to zero immediately so any render that occurs between this
88
+ // effect commit and the first tick shows zeros, not stale values.
89
+ // On initial mount this is a no-op: Object.is bails out because the
90
+ // state was initialized with the same zeroAnimState reference.
91
+ setAnimState(zeroAnimState);
92
+
93
+ let startTime = 0;
94
+
95
+ const { startTime: subscriberStartTime, unsubscribe } = subscribe((currentTime) => {
96
+ const isThrottled = renderThrottleMs > 0 && currentTime < nextRenderTimeRef.current;
97
+
98
+ if (isThrottled) {
99
+ // Coalesce intermediate ticks while Ink is inside the current
100
+ // render-throttle window; the next allowed render will jump
101
+ // straight to the latest elapsed values.
102
+ return;
103
+ }
104
+
105
+ const elapsed = currentTime - startTime;
106
+ const nextDelta = currentTime - lastRenderTimeRef.current;
107
+
108
+ lastRenderTimeRef.current = currentTime;
109
+ nextRenderTimeRef.current = currentTime + renderThrottleMs;
110
+ setAnimState({
111
+ frame: Math.floor(elapsed / safeInterval),
112
+ time: elapsed,
113
+ delta: nextDelta,
114
+ });
115
+ }, safeInterval);
116
+ // Use the scheduler's start time instead of sampling our own clock so the
117
+ // first delivered tick cannot start one frame late.
118
+ startTime = subscriberStartTime;
119
+ lastRenderTimeRef.current = subscriberStartTime;
120
+ nextRenderTimeRef.current = startTime + renderThrottleMs;
121
+
122
+ return unsubscribe;
123
+ }, [safeInterval, isActive, subscribe, renderThrottleMs, resetKey]);
124
+
125
+ useLayoutEffect(() => {
126
+ previousOptionsRef.current = { isActive, safeInterval, resetKey };
127
+ }, [isActive, safeInterval, resetKey]);
128
+
129
+ if (shouldReset) {
130
+ return { ...zeroAnimState, reset };
131
+ }
132
+
133
+ return { ...animState, reset };
134
+ }
135
+
136
+ function normalizeAnimationInterval(interval: number): number {
137
+ if (!Number.isFinite(interval)) {
138
+ return defaultAnimationInterval;
139
+ }
140
+
141
+ return Math.min(maximumTimerInterval, Math.max(1, interval));
142
+ }
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+
3
+ import { AppContext } from "#/components/AppContext.ts";
4
+
5
+ /**
6
+ A React hook that returns app lifecycle methods like `exit()` and `waitUntilRenderFlush()`.
7
+ */
8
+ export const useApp = () => useContext(AppContext);