@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.
- package/README.md +499 -313
- package/THIRD_PARTY_NOTICES.md +70 -23
- package/dist/Text-DV9CuzAT.d.ts +452 -0
- package/dist/ansi.d.ts +217 -0
- package/dist/ansi.js +87 -0
- package/dist/capabilities.d.ts +5 -0
- package/dist/capabilities.js +3 -0
- package/dist/cell-_ZVhbfl0.js +44 -0
- package/dist/color-CkbalRqK.js +2 -0
- package/dist/color-policy-BAC9-TZX.js +592 -0
- package/dist/color-policy-CCxuHIdD.d.ts +22 -0
- package/dist/color-profile-CyeHnG1T.d.ts +97 -0
- package/dist/color-profile-DHhQHY55.js +36 -0
- package/dist/color.d.ts +21 -0
- package/dist/color.js +3 -0
- package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
- package/dist/detect-B3dL4Q11.js +374 -0
- package/dist/detect-Db6GbKOm.d.ts +195 -0
- package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
- package/dist/env-YVw64yZS.js +9 -0
- package/dist/escapes-CB_6CWOE.d.ts +72 -0
- package/dist/geometry-BxXOzJgo.d.ts +11 -0
- package/dist/index-D48vQhhe.d.ts +21 -0
- package/dist/index-DDVME65c.d.ts +919 -0
- package/dist/index.d.ts +1310 -0
- package/dist/index.js +3209 -0
- package/dist/osc-BFKKSqpg.js +71 -0
- package/dist/osc-Cn0fw77g.d.ts +23 -0
- package/dist/paint-Cx-zC_sX.d.ts +81 -0
- package/dist/query-BNc2B8GD.d.ts +152 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +709 -0
- package/dist/sample-Cqw1bjUL.js +445 -0
- package/dist/screen-CgC2WlVM.d.ts +49 -0
- package/dist/screen-CiPytswf.js +342 -0
- package/dist/screen.d.ts +5 -0
- package/dist/screen.js +5 -0
- package/dist/semantic-text-style-DIMzC7xt.js +91 -0
- package/dist/serialize-BTkAZgw1.js +79 -0
- package/dist/session-DDQ5V300.js +723 -0
- package/dist/sgr-BhwaWAJB.js +246 -0
- package/dist/store-C1P5fOUi.d.ts +72 -0
- package/dist/string-width-CijQwpIk.js +69 -0
- package/dist/strip-BvU4toXG.js +6 -0
- package/dist/terminal.d.ts +121 -0
- package/dist/terminal.js +2 -0
- package/dist/tokenize-AjqbvtiT.js +1242 -0
- package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
- package/dist/truncate-D31fhU6i.js +562 -0
- package/dist/use-focus-CcCIdCWA.js +1337 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +70 -24
- package/src/ansi/chalk.ts +138 -0
- package/src/ansi/cursor.ts +46 -0
- package/src/ansi/east-asian-width.ts +259 -0
- package/src/ansi/escapes.ts +120 -0
- package/src/ansi/graphemes.ts +8 -0
- package/src/ansi/hyperlink.ts +44 -0
- package/src/ansi/index.ts +27 -0
- package/src/ansi/osc.ts +77 -0
- package/src/ansi/sgr.ts +234 -0
- package/src/ansi/slice.ts +43 -0
- package/src/ansi/string-width.ts +121 -0
- package/src/ansi/strip.ts +13 -0
- package/src/ansi/tokenize.ts +380 -0
- package/src/ansi/truncate.ts +196 -0
- package/src/ansi/wrap.ts +765 -0
- package/src/ansi-tokenizer.ts +510 -0
- package/src/capabilities/color-policy.ts +34 -0
- package/src/capabilities/detect.ts +608 -0
- package/src/capabilities/index.ts +37 -0
- package/src/capabilities/query.ts +679 -0
- package/src/capabilities/store.ts +394 -0
- package/src/code-excerpt.ts +39 -0
- package/src/color/index.ts +3 -0
- package/src/color/paint.ts +169 -0
- package/src/color/palette.ts +48 -0
- package/src/color/sample.ts +323 -0
- package/src/color.ts +1 -0
- package/src/components/AccessibilityContext.ts +5 -0
- package/src/components/AnimationContext.ts +24 -0
- package/src/components/AnsiText.tsx +42 -0
- package/src/components/App.tsx +878 -0
- package/src/components/AppContext.ts +111 -0
- package/src/components/BackgroundContext.ts +7 -0
- package/src/components/Box.tsx +100 -0
- package/src/components/CursorContext.ts +19 -0
- package/src/components/ErrorBoundary.tsx +39 -0
- package/src/components/ErrorOverview.tsx +135 -0
- package/src/components/FocusContext.ts +30 -0
- package/src/components/Hyperlink.tsx +56 -0
- package/src/components/Newline.tsx +16 -0
- package/src/components/Spacer.tsx +11 -0
- package/src/components/Static.tsx +60 -0
- package/src/components/StderrContext.ts +24 -0
- package/src/components/StdinContext.ts +48 -0
- package/src/components/StdoutContext.ts +26 -0
- package/src/components/TerminalOscContext.ts +25 -0
- package/src/components/Text.tsx +122 -0
- package/src/components/Transform.tsx +38 -0
- package/src/components/VirtualList.tsx +128 -0
- package/src/cursor-position.ts +103 -0
- package/src/devtools.ts +103 -0
- package/src/dom.ts +301 -0
- package/src/env.ts +12 -0
- package/src/get-max-width.ts +11 -0
- package/src/global.d.ts +38 -0
- package/src/glyphs.ts +99 -0
- package/src/hooks/use-animation.ts +142 -0
- package/src/hooks/use-app.ts +8 -0
- package/src/hooks/use-box-metrics.ts +134 -0
- package/src/hooks/use-capabilities.ts +73 -0
- package/src/hooks/use-cursor.ts +33 -0
- package/src/hooks/use-focus-manager.ts +62 -0
- package/src/hooks/use-focus.ts +82 -0
- package/src/hooks/use-input.ts +267 -0
- package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
- package/src/hooks/use-paste.ts +78 -0
- package/src/hooks/use-stderr.ts +8 -0
- package/src/hooks/use-stdin.ts +10 -0
- package/src/hooks/use-stdout.ts +8 -0
- package/src/hooks/use-terminal-osc.ts +59 -0
- package/src/hooks/use-virtual-scroll.ts +84 -0
- package/src/hooks/use-window-size.ts +37 -0
- package/src/index.ts +96 -0
- package/src/ink.tsx +1452 -0
- package/src/input-parser.ts +303 -0
- package/src/instances.ts +9 -0
- package/src/kitty-keyboard.ts +185 -0
- package/src/measure-element.ts +62 -0
- package/src/measure-text.ts +31 -0
- package/src/paint-tree.ts +220 -0
- package/src/parse-keypress.ts +515 -0
- package/src/parse-stack-line.ts +138 -0
- package/src/patch-console.ts +106 -0
- package/src/quick-lru.ts +85 -0
- package/src/reconciler.ts +425 -0
- package/src/render-background.ts +59 -0
- package/src/render-border.ts +167 -0
- package/src/render-frame.ts +83 -0
- package/src/render-to-string.ts +146 -0
- package/src/render.ts +284 -0
- package/src/router/components.tsx +343 -0
- package/src/router/context.ts +41 -0
- package/src/router/history.ts +194 -0
- package/src/router/hooks.tsx +391 -0
- package/src/router/index.ts +34 -0
- package/src/router/matcher.ts +571 -0
- package/src/sanitize-ansi.ts +33 -0
- package/src/screen/ansi.ts +184 -0
- package/src/screen/canvas.ts +160 -0
- package/src/screen/cell.ts +138 -0
- package/src/screen/color-profile.ts +47 -0
- package/src/screen/geometry.ts +9 -0
- package/src/screen/index.ts +6 -0
- package/src/screen/screen.ts +305 -0
- package/src/screen/serialize.ts +129 -0
- package/src/screen.ts +1 -0
- package/src/semantic-text-style.ts +118 -0
- package/src/signal-exit.ts +106 -0
- package/src/squash-text-nodes.ts +37 -0
- package/src/stream.ts +28 -0
- package/src/structured-text.ts +325 -0
- package/src/styles.ts +753 -0
- package/src/terminal/index.ts +2 -0
- package/src/terminal/inline-presenter.ts +120 -0
- package/src/terminal/input.ts +92 -0
- package/src/terminal/render-scheduler.ts +37 -0
- package/src/terminal/screen-presenter.ts +242 -0
- package/src/terminal/session.ts +408 -0
- package/src/terminal-size.ts +58 -0
- package/src/terminal.ts +1 -0
- package/src/testing/browser.ts +588 -0
- package/src/testing/emulators.ts +205 -0
- package/src/testing/explorer-app/index.html +12 -0
- package/src/testing/explorer-app/main.ts +381 -0
- package/src/testing/explorer-app/style.css +194 -0
- package/src/testing/explorer-app/tsconfig.json +15 -0
- package/src/testing/explorer-app/vite-env.d.ts +1 -0
- package/src/testing/index.ts +26 -0
- package/src/testing/keys.ts +56 -0
- package/src/testing/live.ts +85 -0
- package/src/testing/matchers.ts +70 -0
- package/src/testing/public.ts +94 -0
- package/src/testing/terminal.ts +361 -0
- package/src/testing/vitest.ts +157 -0
- package/src/throttle.ts +73 -0
- package/src/transform-adapter.ts +14 -0
- package/src/types.ts +10 -0
- package/src/virtual-scroll.ts +133 -0
- package/src/wrap-text.ts +54 -0
- package/src/yoga/config.ts +57 -0
- package/src/yoga/core/absoluteLayout.ts +626 -0
- package/src/yoga/core/baseline.ts +66 -0
- package/src/yoga/core/cache.ts +136 -0
- package/src/yoga/core/calculateLayout.ts +2926 -0
- package/src/yoga/core/config.ts +104 -0
- package/src/yoga/core/flexLine.ts +177 -0
- package/src/yoga/core/helpers.ts +293 -0
- package/src/yoga/core/layoutResults.ts +167 -0
- package/src/yoga/core/node.ts +611 -0
- package/src/yoga/core/numeric.ts +44 -0
- package/src/yoga/core/pixelGrid.ts +151 -0
- package/src/yoga/core/style.ts +887 -0
- package/src/yoga/core/types.ts +224 -0
- package/src/yoga/generated/YGEnums.ts +263 -0
- package/src/yoga/index.ts +19 -0
- package/src/yoga/node.ts +1140 -0
- package/dist/index.d.mts +0 -2379
- 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
|
+
};
|
package/src/global.d.ts
ADDED
|
@@ -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
|
+
}
|