@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.
- package/README.md +499 -313
- package/THIRD_PARTY_NOTICES.md +98 -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 +3211 -0
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +227 -0
- package/dist/jsx-runtime-XtwIVFsg.js +255 -0
- package/dist/jsx-runtime.d.ts +2 -0
- package/dist/jsx-runtime.js +8 -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/react-CTZ_8dwh.js +1197 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +49 -0
- package/dist/router.d.ts +392 -0
- package/dist/router.js +711 -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-B5npZrJU.js +18736 -0
- package/dist/yoga-5jKhYCJC.js +3465 -0
- package/dist/yoga.d.ts +2 -0
- package/dist/yoga.js +2 -0
- package/package.json +89 -29
- 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/jsx-dev-runtime.ts +3 -0
- package/src/jsx-runtime.ts +6 -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/react.ts +53 -0
- package/src/reconciler.ts +476 -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/render.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Stream, type Writable } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import type { ClipboardSelection, TerminalProgressState } from "#/ansi/osc.ts";
|
|
6
|
+
import { createInk, type Ink, type Options as InkOptions, type RenderMetrics } from "#/ink.tsx";
|
|
7
|
+
import { instances } from "#/instances.ts";
|
|
8
|
+
import { type KittyKeyboardOptions } from "#/kitty-keyboard.ts";
|
|
9
|
+
import { type ColorProfile } from "#/screen/color-profile.ts";
|
|
10
|
+
|
|
11
|
+
export type RenderOptions = {
|
|
12
|
+
/**
|
|
13
|
+
Output stream where the app will be rendered.
|
|
14
|
+
|
|
15
|
+
@default process.stdout
|
|
16
|
+
*/
|
|
17
|
+
stdout?: NodeJS.WritableStream;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Input stream where app will listen for input.
|
|
21
|
+
|
|
22
|
+
@default process.stdin
|
|
23
|
+
*/
|
|
24
|
+
stdin?: NodeJS.ReadableStream;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Error stream.
|
|
28
|
+
@default process.stderr
|
|
29
|
+
*/
|
|
30
|
+
stderr?: NodeJS.WritableStream;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
If true, each update will be rendered as separate output, without replacing the previous one.
|
|
34
|
+
|
|
35
|
+
@default false
|
|
36
|
+
*/
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
Configure whether Ink should listen for Ctrl+C keyboard input and exit the app. This is needed in case `process.stdin` is in raw mode, because then Ctrl+C is ignored by default and the process is expected to handle it manually.
|
|
41
|
+
|
|
42
|
+
@default true
|
|
43
|
+
*/
|
|
44
|
+
exitOnCtrlC?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Patch console methods to ensure console output doesn't mix with Ink's output.
|
|
48
|
+
|
|
49
|
+
Pass `"stdio"` to additionally intercept direct `stdout.write` / `stderr.write` calls on the streams Ink renders to (output from dependencies, native warnings, child tooling). Captured chunks are line-buffered and spliced above the live frame like console output; partial lines are flushed at unmount. Use `onCapturedOutput` to observe captured chunks or take over their display.
|
|
50
|
+
|
|
51
|
+
Note: Once unmount starts, Ink restores the native console (and stream writes) before React cleanup runs. Teardown-time output then follows the normal behavior instead of being rerouted through Ink.
|
|
52
|
+
|
|
53
|
+
@default true
|
|
54
|
+
*/
|
|
55
|
+
patchConsole?: boolean | "stdio";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
Observe output captured by `patchConsole` before Ink displays it.
|
|
59
|
+
|
|
60
|
+
Receives each captured chunk with its origin (`"console"` or `"stdio"`). Return `true` to take ownership of the chunk: Ink will not display it, so the app can render it itself — for example inside a `<Static>` transcript.
|
|
61
|
+
*/
|
|
62
|
+
onCapturedOutput?: (
|
|
63
|
+
stream: "stdout" | "stderr",
|
|
64
|
+
data: string,
|
|
65
|
+
source: "console" | "stdio",
|
|
66
|
+
) => boolean | undefined | void;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
Runs the given callback after each render and re-render with render metrics.
|
|
70
|
+
|
|
71
|
+
Note: this callback runs after Ink commits a frame, but it does not wait for `stdout`/`stderr` stream callbacks.
|
|
72
|
+
To run code after output is flushed, use `waitUntilRenderFlush()`.
|
|
73
|
+
*/
|
|
74
|
+
onRender?: (metrics: RenderMetrics) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
Enable screen reader support. See https://github.com/vadimdemedes/ink/blob/master/readme.md#screen-reader-support
|
|
78
|
+
|
|
79
|
+
@default process.env['SIGIL_SCREEN_READER'] === 'true'
|
|
80
|
+
*/
|
|
81
|
+
isScreenReaderEnabled?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
Maximum frames per second for render updates.
|
|
85
|
+
This controls how frequently the UI can update to prevent excessive re-rendering.
|
|
86
|
+
Higher values allow more frequent updates but may impact performance.
|
|
87
|
+
|
|
88
|
+
@default 30
|
|
89
|
+
*/
|
|
90
|
+
maxFps?: number;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
Override the output color profile for this render instance. When omitted,
|
|
94
|
+
the profile follows the capabilities of `stdout` and capability upgrades
|
|
95
|
+
redraw the live frame. Static content already written to scrollback is not
|
|
96
|
+
replayed or recolored.
|
|
97
|
+
*/
|
|
98
|
+
colorProfile?: ColorProfile;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
Enable React Concurrent Rendering mode.
|
|
102
|
+
|
|
103
|
+
When enabled:
|
|
104
|
+
- Suspense boundaries work correctly with async data
|
|
105
|
+
- `useTransition` and `useDeferredValue` are fully functional
|
|
106
|
+
- Updates can be interrupted for higher priority work
|
|
107
|
+
|
|
108
|
+
Note: Concurrent mode changes the timing of renders. Some tests may need to use `act()` to properly await updates. Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change the rendering mode or create a fresh instance.
|
|
109
|
+
|
|
110
|
+
@default false
|
|
111
|
+
*/
|
|
112
|
+
concurrent?: boolean;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
Configure kitty keyboard protocol support for enhanced keyboard input.
|
|
116
|
+
Enables additional modifiers (super, hyper, capsLock, numLock) and
|
|
117
|
+
disambiguated key events in terminals that support the protocol.
|
|
118
|
+
|
|
119
|
+
@see https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
|
120
|
+
*/
|
|
121
|
+
kittyKeyboard?: KittyKeyboardOptions;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
Override automatic interactive mode detection.
|
|
125
|
+
|
|
126
|
+
By default, Ink detects whether the environment is interactive based on CI detection (the `CI` environment variable) and `stdout.isTTY`. Most users should not need to set this.
|
|
127
|
+
|
|
128
|
+
When non-interactive, Ink disables ANSI erase sequences, cursor manipulation, synchronized output, resize handling, and kitty keyboard auto-detection, writing only the final frame at unmount.
|
|
129
|
+
|
|
130
|
+
Set to `false` to force non-interactive mode or `true` to force interactive mode when the automatic detection doesn't suit your use case.
|
|
131
|
+
|
|
132
|
+
Note: Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change this option or create a fresh instance.
|
|
133
|
+
|
|
134
|
+
@default true (false if in CI or `stdout.isTTY` is falsy)
|
|
135
|
+
*/
|
|
136
|
+
interactive?: boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
Render the app in the terminal's alternate screen buffer. When enabled, the app renders on a separate screen, and the original terminal content is restored when the app exits. This is the same mechanism used by programs like vim, htop, and less.
|
|
140
|
+
|
|
141
|
+
Note: The terminal's scrollback buffer is not available while in the alternate screen. This is standard terminal behavior; programs like vim use the alternate screen specifically to avoid polluting the user's scrollback history.
|
|
142
|
+
|
|
143
|
+
Note: Ink intentionally treats alternate-screen teardown output as disposable. It does not preserve or replay teardown-time frames, hook writes, or `console.*` output after restoring the primary screen.
|
|
144
|
+
|
|
145
|
+
Only works in interactive mode. Ignored when `interactive` is `false` or in a non-interactive environment (CI, piped stdout).
|
|
146
|
+
|
|
147
|
+
Note: Reusing the same stdout across multiple `render()` calls without unmounting is unsupported. Call `unmount()` first if you need to change this option or create a fresh instance.
|
|
148
|
+
|
|
149
|
+
@default false
|
|
150
|
+
*/
|
|
151
|
+
alternateScreen?: boolean;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type Instance = {
|
|
155
|
+
/**
|
|
156
|
+
Replace the previous root node with a new one or update props of the current root node.
|
|
157
|
+
*/
|
|
158
|
+
rerender: Ink["render"];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
Manually unmount the whole Ink app.
|
|
162
|
+
*/
|
|
163
|
+
unmount: Ink["unmount"];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
Returns a promise that settles when the app is unmounted.
|
|
167
|
+
|
|
168
|
+
It resolves with the value passed to `exit(value)` and rejects with the error passed to `exit(error)`.
|
|
169
|
+
When `unmount()` is called manually, it settles after unmount-related stdout writes complete.
|
|
170
|
+
|
|
171
|
+
@example
|
|
172
|
+
```jsx
|
|
173
|
+
const {unmount, waitUntilExit} = render(<MyApp />);
|
|
174
|
+
|
|
175
|
+
setTimeout(unmount, 1000);
|
|
176
|
+
|
|
177
|
+
await waitUntilExit(); // resolves after `unmount()` is called
|
|
178
|
+
```
|
|
179
|
+
*/
|
|
180
|
+
waitUntilExit: Ink["waitUntilExit"];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
Returns a promise that settles after pending render output is flushed to stdout.
|
|
184
|
+
|
|
185
|
+
This can be used after `rerender()` when you need to run code only after the frame is written.
|
|
186
|
+
|
|
187
|
+
@example
|
|
188
|
+
```jsx
|
|
189
|
+
const {rerender, waitUntilRenderFlush} = render(<MyApp step="loading" />);
|
|
190
|
+
|
|
191
|
+
rerender(<MyApp step="ready" />);
|
|
192
|
+
await waitUntilRenderFlush(); // output for "ready" is flushed
|
|
193
|
+
|
|
194
|
+
runNextCommand();
|
|
195
|
+
```
|
|
196
|
+
*/
|
|
197
|
+
waitUntilRenderFlush: Ink["waitUntilRenderFlush"];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
Unmount the current app and remove the internal Ink instance for this stdout.
|
|
201
|
+
|
|
202
|
+
This is mostly useful for advanced cases where you need `render()` to create a fresh instance for the same stream without leaving terminal state such as the alternate screen behind.
|
|
203
|
+
*/
|
|
204
|
+
cleanup: () => void;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
Clear output.
|
|
208
|
+
*/
|
|
209
|
+
clear: () => void;
|
|
210
|
+
|
|
211
|
+
/** Copy text through the renderer-owned terminal session. */
|
|
212
|
+
copyToClipboard: (text: string, selection?: ClipboardSelection) => boolean;
|
|
213
|
+
|
|
214
|
+
/** Update terminal-native progress through the renderer-owned session. */
|
|
215
|
+
setProgress: (state: TerminalProgressState, value?: number) => boolean;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
Mount a component and render the output.
|
|
220
|
+
*/
|
|
221
|
+
export const render = (node: ReactNode, options?: Writable | RenderOptions): Instance => {
|
|
222
|
+
const inkOptions: InkOptions = {
|
|
223
|
+
stdout: process.stdout,
|
|
224
|
+
stdin: process.stdin,
|
|
225
|
+
stderr: process.stderr,
|
|
226
|
+
debug: false,
|
|
227
|
+
exitOnCtrlC: true,
|
|
228
|
+
patchConsole: true,
|
|
229
|
+
maxFps: 30,
|
|
230
|
+
concurrent: false,
|
|
231
|
+
alternateScreen: false,
|
|
232
|
+
...getOptions(options),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const instance: Ink = getInstance(inkOptions.stdout, () => createInk(inkOptions));
|
|
236
|
+
instance.render(node);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
rerender: instance.render,
|
|
240
|
+
unmount() {
|
|
241
|
+
instance.unmount();
|
|
242
|
+
},
|
|
243
|
+
waitUntilExit: instance.waitUntilExit,
|
|
244
|
+
waitUntilRenderFlush: instance.waitUntilRenderFlush,
|
|
245
|
+
cleanup() {
|
|
246
|
+
instance.unmount();
|
|
247
|
+
},
|
|
248
|
+
clear: instance.clear,
|
|
249
|
+
copyToClipboard: instance.copyToClipboard,
|
|
250
|
+
setProgress: instance.setProgress,
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const getOptions = (stdout: Writable | RenderOptions | undefined = {}): RenderOptions => {
|
|
255
|
+
if (stdout instanceof Stream) {
|
|
256
|
+
return {
|
|
257
|
+
stdout,
|
|
258
|
+
stdin: process.stdin,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return stdout;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const getInstance = (stdout: NodeJS.WritableStream, createInstance: () => Ink): Ink => {
|
|
266
|
+
const instance = instances.get(stdout);
|
|
267
|
+
|
|
268
|
+
if (instance === undefined) {
|
|
269
|
+
const newInstance = createInstance();
|
|
270
|
+
instances.set(stdout, newInstance);
|
|
271
|
+
return newInstance;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Ink keeps one live renderer per stdout. Reusing the same stream without
|
|
275
|
+
// unmounting is unsupported, but return the existing instance so we don't
|
|
276
|
+
// create two renderers that compete for the same output. Write the warning
|
|
277
|
+
// directly to native stderr so an existing alternate-screen renderer cannot
|
|
278
|
+
// swallow it via patchConsole.
|
|
279
|
+
process.stderr.write(
|
|
280
|
+
"Warning: render() was called again for the same stdout before the previous Ink instance was unmounted. Reusing stdout across multiple render() calls is unsupported. Call unmount() first.\n",
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return instance;
|
|
284
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
// MemoryRouter/Routes/Route/Navigate/Outlet are ported from react-router's
|
|
3
|
+
// declarative components (lib/components.tsx, MIT). Link is Sigil-native.
|
|
4
|
+
import {
|
|
5
|
+
Children,
|
|
6
|
+
Fragment,
|
|
7
|
+
isValidElement,
|
|
8
|
+
startTransition,
|
|
9
|
+
useCallback,
|
|
10
|
+
useEffect,
|
|
11
|
+
useLayoutEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
type ReactElement,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
} from "react";
|
|
18
|
+
|
|
19
|
+
import { Text, type Props as TextProps } from "#/components/Text.tsx";
|
|
20
|
+
import { useFocus } from "#/hooks/use-focus.ts";
|
|
21
|
+
import { useInput } from "#/hooks/use-input.ts";
|
|
22
|
+
import { LocationContext, NavigationContext, type Navigator } from "#/router/context.ts";
|
|
23
|
+
import {
|
|
24
|
+
createMemoryHistory,
|
|
25
|
+
type InitialEntry,
|
|
26
|
+
type Location,
|
|
27
|
+
type To,
|
|
28
|
+
} from "#/router/history.ts";
|
|
29
|
+
import {
|
|
30
|
+
useLocation,
|
|
31
|
+
useNavigate,
|
|
32
|
+
useOutlet,
|
|
33
|
+
useResolvedPath,
|
|
34
|
+
useRoutes,
|
|
35
|
+
} from "#/router/hooks.tsx";
|
|
36
|
+
import { normalizePathname, type RouteObject } from "#/router/matcher.ts";
|
|
37
|
+
|
|
38
|
+
export type MemoryRouterProps = {
|
|
39
|
+
/**
|
|
40
|
+
The navigation stack to start with. Defaults to `["/"]`.
|
|
41
|
+
*/
|
|
42
|
+
initialEntries?: InitialEntry[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
The index of the initial entry to render. Defaults to the last entry.
|
|
46
|
+
*/
|
|
47
|
+
initialIndex?: number;
|
|
48
|
+
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
The routing container for a Sigil app. Stores the navigation stack in memory —
|
|
54
|
+
routes aren't URLs, they're screen states.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<MemoryRouter>
|
|
58
|
+
<Routes>
|
|
59
|
+
<Route path="/" element={<Home />} />
|
|
60
|
+
<Route path="/settings" element={<Settings />} />
|
|
61
|
+
</Routes>
|
|
62
|
+
</MemoryRouter>
|
|
63
|
+
```
|
|
64
|
+
*/
|
|
65
|
+
export function MemoryRouter({ initialEntries, initialIndex, children }: MemoryRouterProps) {
|
|
66
|
+
const historyRef = useRef<ReturnType<typeof createMemoryHistory> | null>(null);
|
|
67
|
+
historyRef.current ??= createMemoryHistory({ initialEntries, initialIndex });
|
|
68
|
+
|
|
69
|
+
const history = historyRef.current;
|
|
70
|
+
const [state, setState] = useState({
|
|
71
|
+
action: history.action,
|
|
72
|
+
location: history.location,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Navigation is a transition: if the destination screen suspends, the
|
|
76
|
+
// current screen stays visible until it's ready.
|
|
77
|
+
useLayoutEffect(
|
|
78
|
+
() =>
|
|
79
|
+
history.listen(({ action, location }) => {
|
|
80
|
+
startTransition(() => {
|
|
81
|
+
setState({ action, location });
|
|
82
|
+
});
|
|
83
|
+
}),
|
|
84
|
+
[history],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const navigator = useMemo<Navigator>(
|
|
88
|
+
() => ({
|
|
89
|
+
push: (to, historyState) => history.push(to, historyState),
|
|
90
|
+
replace: (to, historyState) => history.replace(to, historyState),
|
|
91
|
+
go: (delta) => history.go(delta),
|
|
92
|
+
canGoBack: () => history.canGoBack,
|
|
93
|
+
canGoForward: () => history.canGoForward,
|
|
94
|
+
}),
|
|
95
|
+
[history],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const navigationContext = useMemo(() => ({ navigator }), [navigator]);
|
|
99
|
+
const locationContext = useMemo(
|
|
100
|
+
() => ({ location: state.location, navigationType: state.action }),
|
|
101
|
+
[state],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<NavigationContext.Provider value={navigationContext}>
|
|
106
|
+
<LocationContext.Provider value={locationContext}>{children}</LocationContext.Provider>
|
|
107
|
+
</NavigationContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type RouteProps = {
|
|
112
|
+
/**
|
|
113
|
+
The path pattern to match, relative to the parent route. Supports `:param`
|
|
114
|
+
dynamic segments, optional segments (`:param?`, `edit?`), and a trailing
|
|
115
|
+
`*` splat.
|
|
116
|
+
*/
|
|
117
|
+
path?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
Render this route in the parent's `<Outlet>` at the parent's exact path.
|
|
121
|
+
Index routes cannot have children.
|
|
122
|
+
*/
|
|
123
|
+
index?: boolean;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
The element to render when this route matches.
|
|
127
|
+
*/
|
|
128
|
+
element?: ReactNode;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
Nested `<Route>` elements, rendered into this route's `<Outlet>`.
|
|
132
|
+
*/
|
|
133
|
+
children?: ReactNode;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
Declares a route. Only valid as a child of `<Routes>` or another `<Route>`.
|
|
138
|
+
*/
|
|
139
|
+
export function Route(_props: RouteProps): ReactElement | null {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"A <Route> is only ever to be used as the child of a <Routes> element, " +
|
|
142
|
+
"never rendered directly. Please wrap your <Route> in a <Routes>.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createRoutesFromChildren(children: ReactNode): RouteObject[] {
|
|
147
|
+
const routes: RouteObject[] = [];
|
|
148
|
+
|
|
149
|
+
Children.forEach(children, (element) => {
|
|
150
|
+
if (!isValidElement(element)) {
|
|
151
|
+
// Ignore non-elements. This allows people to more easily inline
|
|
152
|
+
// conditionals in their route config.
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (element.type === Fragment) {
|
|
157
|
+
// Transparently support React.Fragment and its children.
|
|
158
|
+
routes.push(
|
|
159
|
+
...createRoutesFromChildren((element.props as { children?: ReactNode }).children),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (element.type !== Route) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`[${typeof element.type === "string" ? element.type : ((element.type as { name?: string }).name ?? "unknown")}] ` +
|
|
167
|
+
`is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const props = element.props as RouteProps;
|
|
172
|
+
const route: RouteObject = {
|
|
173
|
+
path: props.path,
|
|
174
|
+
index: props.index,
|
|
175
|
+
element: props.element,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (props.children) {
|
|
179
|
+
route.children = createRoutesFromChildren(props.children);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
routes.push(route);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return routes;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type RoutesProps = {
|
|
189
|
+
children?: ReactNode;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
Match against this location instead of the current one. Useful for
|
|
193
|
+
rendering a screen other than the one navigated to (e.g. transitions).
|
|
194
|
+
*/
|
|
195
|
+
location?: Partial<Location> | string;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
Renders the branch of child `<Route>` elements that best matches the current
|
|
200
|
+
location.
|
|
201
|
+
*/
|
|
202
|
+
export function Routes({ children, location }: RoutesProps): ReactElement | null {
|
|
203
|
+
return useRoutes(createRoutesFromChildren(children), location);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type OutletProps = {
|
|
207
|
+
/**
|
|
208
|
+
A value to make available to descendant routes via `useOutletContext()`.
|
|
209
|
+
*/
|
|
210
|
+
context?: unknown;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
Renders the matching child route of a parent route, or nothing if no child
|
|
215
|
+
matches.
|
|
216
|
+
*/
|
|
217
|
+
export function Outlet(props: OutletProps): ReactElement | null {
|
|
218
|
+
return useOutlet(props.context);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export type NavigateProps = {
|
|
222
|
+
to: To;
|
|
223
|
+
replace?: boolean;
|
|
224
|
+
state?: unknown;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
Navigates as soon as it renders. The component form of `useNavigate`, for
|
|
229
|
+
declarative redirects:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
<Route path="/" element={<Navigate to="/home" replace />} />
|
|
233
|
+
```
|
|
234
|
+
*/
|
|
235
|
+
export function Navigate({ to, replace, state }: NavigateProps): null {
|
|
236
|
+
const navigate = useNavigate();
|
|
237
|
+
const { pathname, search } = useResolvedPath(to);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
navigate({ pathname, search }, { replace, state });
|
|
241
|
+
}, [navigate, pathname, search, replace, state]);
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type LinkRenderState = {
|
|
247
|
+
/**
|
|
248
|
+
Whether this link currently has focus.
|
|
249
|
+
*/
|
|
250
|
+
isFocused: boolean;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
Whether the current location is the link's destination or a descendant of
|
|
254
|
+
it.
|
|
255
|
+
*/
|
|
256
|
+
isActive: boolean;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export type LinkProps = Omit<TextProps, "children"> & {
|
|
260
|
+
/**
|
|
261
|
+
The destination to navigate to when the link is activated.
|
|
262
|
+
*/
|
|
263
|
+
to: To;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
Replace the current entry in the navigation stack instead of pushing.
|
|
267
|
+
*/
|
|
268
|
+
replace?: boolean;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
State to attach to the destination location.
|
|
272
|
+
*/
|
|
273
|
+
state?: unknown;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
Focus this link if nothing else is focused yet.
|
|
277
|
+
*/
|
|
278
|
+
autoFocus?: boolean;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
An ID for programmatic focus via `useFocusManager().focus(id)`.
|
|
282
|
+
*/
|
|
283
|
+
id?: string;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
Link content. Pass a function to take full control of rendering based on
|
|
287
|
+
focus and active state.
|
|
288
|
+
*/
|
|
289
|
+
children?: ReactNode | ((state: LinkRenderState) => ReactNode);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
A focusable navigation element — the terminal's `<a>` tag. Focus it with
|
|
294
|
+
<kbd>Tab</kbd> and activate it with <kbd>Enter</kbd>. By default the focused
|
|
295
|
+
link renders inverse; pass a function as `children` (or any `Text` props) to
|
|
296
|
+
customize.
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
<Link to="/settings">Settings</Link>
|
|
300
|
+
```
|
|
301
|
+
*/
|
|
302
|
+
export function Link({
|
|
303
|
+
to,
|
|
304
|
+
replace = false,
|
|
305
|
+
state,
|
|
306
|
+
autoFocus = false,
|
|
307
|
+
id,
|
|
308
|
+
children,
|
|
309
|
+
...textProps
|
|
310
|
+
}: LinkProps) {
|
|
311
|
+
const navigate = useNavigate();
|
|
312
|
+
const path = useResolvedPath(to);
|
|
313
|
+
const { pathname: locationPathname } = useLocation();
|
|
314
|
+
const { isFocused } = useFocus({ autoFocus, id });
|
|
315
|
+
|
|
316
|
+
const toPathname = normalizePathname(path.pathname);
|
|
317
|
+
const isActive =
|
|
318
|
+
locationPathname === toPathname ||
|
|
319
|
+
(locationPathname.startsWith(toPathname) && locationPathname.charAt(toPathname.length) === "/");
|
|
320
|
+
|
|
321
|
+
const activate = useCallback(() => {
|
|
322
|
+
navigate({ pathname: path.pathname, search: path.search }, { replace, state });
|
|
323
|
+
}, [navigate, path.pathname, path.search, replace, state]);
|
|
324
|
+
|
|
325
|
+
useInput(
|
|
326
|
+
(_input, key) => {
|
|
327
|
+
if (key.return) {
|
|
328
|
+
activate();
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{ isActive: isFocused },
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (typeof children === "function") {
|
|
335
|
+
return <>{children({ isFocused, isActive })}</>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<Text inverse={isFocused} {...textProps}>
|
|
340
|
+
{children}
|
|
341
|
+
</Text>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createContext, type ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
import { type Location, type NavigationType, type Path } from "#/router/history.ts";
|
|
4
|
+
import { type RouteMatch } from "#/router/matcher.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
The imperative interface `useNavigate` drives. Backed by the memory history
|
|
8
|
+
inside `<MemoryRouter>`.
|
|
9
|
+
*/
|
|
10
|
+
export type Navigator = {
|
|
11
|
+
push: (to: Path, state?: unknown) => void;
|
|
12
|
+
replace: (to: Path, state?: unknown) => void;
|
|
13
|
+
go: (delta: number) => void;
|
|
14
|
+
canGoBack: () => boolean;
|
|
15
|
+
canGoForward: () => boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type NavigationContextObject = {
|
|
19
|
+
navigator: Navigator;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const NavigationContext = createContext<NavigationContextObject | null>(null);
|
|
23
|
+
|
|
24
|
+
export type LocationContextObject = {
|
|
25
|
+
location: Location;
|
|
26
|
+
navigationType: NavigationType;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const LocationContext = createContext<LocationContextObject | null>(null);
|
|
30
|
+
|
|
31
|
+
export type RouteContextObject = {
|
|
32
|
+
outlet: ReactElement | null;
|
|
33
|
+
matches: RouteMatch[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const RouteContext = createContext<RouteContextObject>({
|
|
37
|
+
outlet: null,
|
|
38
|
+
matches: [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const OutletContext = createContext<unknown>(null);
|