@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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
import type { ClipboardSelection, TerminalProgressState } from "#/ansi/osc.ts";
|
|
4
|
+
|
|
5
|
+
export type TerminalOsc = {
|
|
6
|
+
readonly publishProgress: (owner: symbol, state: TerminalProgressState, value?: number) => void;
|
|
7
|
+
readonly copyToClipboard: (text: string, selection?: ClipboardSelection) => void;
|
|
8
|
+
readonly publishTitle: (owner: symbol, title?: string) => void;
|
|
9
|
+
readonly setWorkingDirectory: (directory: URL | string) => void;
|
|
10
|
+
readonly notify: (title: string) => void;
|
|
11
|
+
readonly setPointerShape: (shape: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const noop = () => {};
|
|
15
|
+
|
|
16
|
+
export const TerminalOscContext = createContext<TerminalOsc>({
|
|
17
|
+
publishProgress: noop,
|
|
18
|
+
copyToClipboard: noop,
|
|
19
|
+
publishTitle: noop,
|
|
20
|
+
setWorkingDirectory: noop,
|
|
21
|
+
notify: noop,
|
|
22
|
+
setPointerShape: noop,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
TerminalOscContext.displayName = "InternalTerminalOscContext";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useContext, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import type { Paint } from "#/color/paint.ts";
|
|
5
|
+
import { accessibilityContext } from "#/components/AccessibilityContext.ts";
|
|
6
|
+
import { cellAttributes } from "#/screen/cell.ts";
|
|
7
|
+
import { parseSemanticColor } from "#/semantic-text-style.ts";
|
|
8
|
+
import { type Styles } from "#/styles.ts";
|
|
9
|
+
|
|
10
|
+
export type Props = {
|
|
11
|
+
/**
|
|
12
|
+
A label for the element for screen readers.
|
|
13
|
+
*/
|
|
14
|
+
readonly "aria-label"?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
Hide the element from screen readers.
|
|
18
|
+
*/
|
|
19
|
+
readonly "aria-hidden"?: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
Change text color. Ink uses Chalk under the hood, so all its functionality is supported.
|
|
23
|
+
*/
|
|
24
|
+
readonly color?: Paint;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Same as `color`, but for the background.
|
|
28
|
+
*/
|
|
29
|
+
readonly backgroundColor?: Paint;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Dim the color (make it less bright).
|
|
33
|
+
*/
|
|
34
|
+
readonly dimColor?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
Make the text bold.
|
|
38
|
+
*/
|
|
39
|
+
readonly bold?: boolean;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
Make the text italic.
|
|
43
|
+
*/
|
|
44
|
+
readonly italic?: boolean;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
Make the text underlined.
|
|
48
|
+
*/
|
|
49
|
+
readonly underline?: boolean;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
Make the text crossed out with a line.
|
|
53
|
+
*/
|
|
54
|
+
readonly strikethrough?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
Inverse background and foreground colors.
|
|
58
|
+
*/
|
|
59
|
+
readonly inverse?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
This property tells Ink to wrap or truncate text if its width is larger than the container. If `wrap` is passed (the default), Ink will wrap text and split it into multiple lines. If `hard` is passed, Ink will fill each line to the full column width, breaking words as necessary. If `truncate-*` is passed, Ink will truncate text instead, resulting in one line of text with the rest cut off. If `none` is passed, the text is neither wrapped nor truncated — it overflows the container (and the screen's nominal width), which is intended for log lines committed to scrollback (e.g. inside `<Static>`), not for live regions.
|
|
63
|
+
*/
|
|
64
|
+
readonly wrap?: Styles["textWrap"];
|
|
65
|
+
|
|
66
|
+
readonly children?: ReactNode;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
This component can display text and change its style to make it bold, underlined, italic, or strikethrough.
|
|
71
|
+
*/
|
|
72
|
+
export function Text({
|
|
73
|
+
color,
|
|
74
|
+
backgroundColor,
|
|
75
|
+
dimColor = false,
|
|
76
|
+
bold = false,
|
|
77
|
+
italic = false,
|
|
78
|
+
underline = false,
|
|
79
|
+
strikethrough = false,
|
|
80
|
+
inverse = false,
|
|
81
|
+
wrap = "wrap",
|
|
82
|
+
children,
|
|
83
|
+
"aria-label": ariaLabel,
|
|
84
|
+
"aria-hidden": ariaHidden = false,
|
|
85
|
+
}: Props) {
|
|
86
|
+
const { isScreenReaderEnabled } = useContext(accessibilityContext);
|
|
87
|
+
const childrenOrAriaLabel = isScreenReaderEnabled && ariaLabel ? ariaLabel : children;
|
|
88
|
+
|
|
89
|
+
if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const foreground = typeof color === "object" ? color : parseSemanticColor(color);
|
|
94
|
+
const background =
|
|
95
|
+
typeof backgroundColor === "object" ? backgroundColor : parseSemanticColor(backgroundColor);
|
|
96
|
+
const semanticStyle = {
|
|
97
|
+
...(foreground ? { foreground } : {}),
|
|
98
|
+
...(background ? { background } : {}),
|
|
99
|
+
...(color === "" ? { resetForeground: true } : {}),
|
|
100
|
+
...(backgroundColor === "" ? { resetBackground: true } : {}),
|
|
101
|
+
...(underline ? { underline: "single" as const } : {}),
|
|
102
|
+
attributes:
|
|
103
|
+
(dimColor ? cellAttributes.faint : 0) +
|
|
104
|
+
(bold ? cellAttributes.bold : 0) +
|
|
105
|
+
(italic ? cellAttributes.italic : 0) +
|
|
106
|
+
(strikethrough ? cellAttributes.strikethrough : 0) +
|
|
107
|
+
(inverse ? cellAttributes.inverse : 0),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (isScreenReaderEnabled && ariaHidden) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ink-text
|
|
116
|
+
style={{ flexGrow: 0, flexShrink: 1, flexDirection: "row", textWrap: wrap }}
|
|
117
|
+
internal_textStyle={semanticStyle}
|
|
118
|
+
>
|
|
119
|
+
{childrenOrAriaLabel}
|
|
120
|
+
</ink-text>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useContext, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { accessibilityContext } from "#/components/AccessibilityContext.ts";
|
|
5
|
+
|
|
6
|
+
export type Props = {
|
|
7
|
+
/**
|
|
8
|
+
Screen-reader-specific text to output. If this is set, all children will be ignored.
|
|
9
|
+
*/
|
|
10
|
+
readonly accessibilityLabel?: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
Compatibility function that transforms the ANSI serialization of this subtree.
|
|
14
|
+
*/
|
|
15
|
+
readonly transform: (children: string, index: number) => string;
|
|
16
|
+
|
|
17
|
+
readonly children?: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
Transform a string representation of React components before they're written to output. For example, you might want to apply a gradient to text, add a clickable link, or create some text effects. These use cases can't accept React nodes as input; they expect a string. That's what the <Transform> component does: it gives you an output string of its child components and lets you transform it in any way.
|
|
22
|
+
*/
|
|
23
|
+
export function Transform({ children, transform, accessibilityLabel }: Props) {
|
|
24
|
+
const { isScreenReaderEnabled } = useContext(accessibilityContext);
|
|
25
|
+
|
|
26
|
+
if (children === undefined || children === null) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<ink-text
|
|
32
|
+
style={{ flexGrow: 0, flexShrink: 1, flexDirection: "row" }}
|
|
33
|
+
internal_transform={transform}
|
|
34
|
+
>
|
|
35
|
+
{isScreenReaderEnabled && accessibilityLabel ? accessibilityLabel : children}
|
|
36
|
+
</ink-text>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState, type Key, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { Box } from "#/components/Box.tsx";
|
|
5
|
+
import { addLayoutListener, type DOMElement } from "#/dom.ts";
|
|
6
|
+
import { useVirtualScroll } from "#/hooks/use-virtual-scroll.ts";
|
|
7
|
+
|
|
8
|
+
export type Props<Item> = {
|
|
9
|
+
/**
|
|
10
|
+
Items to window over.
|
|
11
|
+
*/
|
|
12
|
+
readonly items: ReadonlyArray<Item>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
Height of an item in rows. It must match what `renderItem` produces for it:
|
|
16
|
+
the window is computed from these numbers, never from the rendered output.
|
|
17
|
+
*/
|
|
18
|
+
readonly itemHeight: (item: Item, index: number) => number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
Render one item. Only items intersecting the viewport are rendered.
|
|
22
|
+
*/
|
|
23
|
+
readonly renderItem: (item: Item, index: number) => ReactNode;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
React key for an item. Defaults to its index.
|
|
27
|
+
*/
|
|
28
|
+
readonly getKey?: (item: Item, index: number) => Key;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
Item to keep fully visible. When it changes, the list scrolls as little as
|
|
32
|
+
necessary to show it.
|
|
33
|
+
*/
|
|
34
|
+
readonly focusedIndex?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
Viewport height in rows. When omitted the list takes the height of its
|
|
38
|
+
content and shrinks to whatever space its container leaves: bound an
|
|
39
|
+
ancestor (`height` or `maxHeight`) and give the siblings that must keep
|
|
40
|
+
their size `flexShrink={0}`.
|
|
41
|
+
*/
|
|
42
|
+
readonly height?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const rootOf = (node: DOMElement | null): DOMElement | undefined => {
|
|
46
|
+
let current = node;
|
|
47
|
+
while (current?.parentNode) {
|
|
48
|
+
current = current.parentNode;
|
|
49
|
+
}
|
|
50
|
+
return current?.nodeName === "ink-root" ? current : undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
A vertically windowed list: only the items intersecting the viewport are
|
|
55
|
+
rendered, inside a clipped box that scrolls by whole rows. Items may have
|
|
56
|
+
different heights, and the item at the top edge may be partially visible.
|
|
57
|
+
|
|
58
|
+
Until the first layout pass has measured the viewport, every item is rendered
|
|
59
|
+
inside the clipped box so the first frame already looks right.
|
|
60
|
+
*/
|
|
61
|
+
export function VirtualList<Item>({
|
|
62
|
+
items,
|
|
63
|
+
itemHeight,
|
|
64
|
+
renderItem,
|
|
65
|
+
getKey,
|
|
66
|
+
focusedIndex,
|
|
67
|
+
height,
|
|
68
|
+
}: Props<Item>) {
|
|
69
|
+
const ref = useRef<DOMElement>(null);
|
|
70
|
+
const [measured, setMeasured] = useState<number>();
|
|
71
|
+
|
|
72
|
+
const measure = () => {
|
|
73
|
+
const next = ref.current?.yogaNode?.getComputedHeight();
|
|
74
|
+
if (next !== undefined) {
|
|
75
|
+
setMeasured((previous) => (previous === next ? previous : next));
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Yoga has laid out the tree by the time layout effects run, so the height
|
|
80
|
+
// this box ended up with is readable synchronously after every commit.
|
|
81
|
+
useLayoutEffect(() => {
|
|
82
|
+
if (height === undefined) measure();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Sibling-driven changes (a resize, chrome growing) re-layout without
|
|
86
|
+
// re-rendering this component; follow the root's layout commits for those.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (height !== undefined) return;
|
|
89
|
+
const root = rootOf(ref.current);
|
|
90
|
+
return root ? addLayoutListener(root, measure) : undefined;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const viewportHeight = height ?? measured;
|
|
94
|
+
const windowed = viewportHeight !== undefined;
|
|
95
|
+
const window = useVirtualScroll({
|
|
96
|
+
count: items.length,
|
|
97
|
+
itemHeight: (index) => itemHeight(items[index]!, index),
|
|
98
|
+
viewportHeight: viewportHeight ?? 0,
|
|
99
|
+
// Following the focus against an unmeasured (zero-row) viewport would pin
|
|
100
|
+
// the item to the top; wait for the real height so it moves minimally.
|
|
101
|
+
focusedIndex: windowed ? focusedIndex : undefined,
|
|
102
|
+
});
|
|
103
|
+
const start = windowed ? window.start : 0;
|
|
104
|
+
const end = windowed ? window.end : items.length;
|
|
105
|
+
const offset = windowed ? window.offset : 0;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Box
|
|
109
|
+
ref={ref}
|
|
110
|
+
flexDirection="column"
|
|
111
|
+
overflowY="hidden"
|
|
112
|
+
{...(height === undefined
|
|
113
|
+
? { height: window.totalHeight, minHeight: 0, flexShrink: 1 }
|
|
114
|
+
: { height, flexShrink: 0 })}
|
|
115
|
+
>
|
|
116
|
+
<Box flexDirection="column" flexShrink={0} marginTop={offset}>
|
|
117
|
+
{items.slice(start, end).map((item, sliceIndex) => {
|
|
118
|
+
const index = start + sliceIndex;
|
|
119
|
+
return (
|
|
120
|
+
<Box key={getKey ? getKey(item, index) : index} flexDirection="column" flexShrink={0}>
|
|
121
|
+
{renderItem(item, index)}
|
|
122
|
+
</Box>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</Box>
|
|
126
|
+
</Box>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ansiEscapes } from "#/ansi/escapes.ts";
|
|
2
|
+
|
|
3
|
+
export type CursorPosition = {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const showCursorEscape = ansiEscapes.cursorShow;
|
|
9
|
+
export const hideCursorEscape = ansiEscapes.cursorHide;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Compare two cursor positions. Returns true if they differ.
|
|
13
|
+
*/
|
|
14
|
+
export const cursorPositionChanged = (
|
|
15
|
+
a: CursorPosition | undefined,
|
|
16
|
+
b: CursorPosition | undefined,
|
|
17
|
+
): boolean => a?.x !== b?.x || a?.y !== b?.y;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
Build escape sequence to move cursor from the bottom of the output to the target position and show it.
|
|
21
|
+
|
|
22
|
+
`bottomLine` is the row the renderer left the cursor on, counted from the top of the output.
|
|
23
|
+
That is always `lines.length - 1` for `lines = str.split('\n')`, whether or not the output ends
|
|
24
|
+
with a newline:
|
|
25
|
+
|
|
26
|
+
- With a trailing newline, `split` yields one extra empty element and the renderer stops just
|
|
27
|
+
past the last visible line — which is `lines.length - 1`.
|
|
28
|
+
- Without one, there is no extra element and the renderer deliberately stops on the last visible
|
|
29
|
+
line instead of moving past it — which is also `lines.length - 1`.
|
|
30
|
+
|
|
31
|
+
This is the same row basis `buildReturnToBottom` measures from, so the two stay in step.
|
|
32
|
+
*/
|
|
33
|
+
export const buildCursorSuffix = (
|
|
34
|
+
bottomLine: number,
|
|
35
|
+
cursorPosition: CursorPosition | undefined,
|
|
36
|
+
): string => {
|
|
37
|
+
if (!cursorPosition) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const moveUp = bottomLine - cursorPosition.y;
|
|
42
|
+
return (
|
|
43
|
+
(moveUp > 0 ? ansiEscapes.cursorUp(moveUp) : "") +
|
|
44
|
+
ansiEscapes.cursorTo(cursorPosition.x) +
|
|
45
|
+
showCursorEscape
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
Build escape sequence to move cursor from previousCursorPosition back to the bottom of output.
|
|
51
|
+
This must be done before eraseLines or any operation that assumes cursor is at the bottom.
|
|
52
|
+
*/
|
|
53
|
+
export const buildReturnToBottom = (
|
|
54
|
+
previousLineCount: number,
|
|
55
|
+
previousCursorPosition: CursorPosition | undefined,
|
|
56
|
+
): string => {
|
|
57
|
+
if (!previousCursorPosition) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// PreviousLineCount is the raw `split('\n')` length, so `previousLineCount - 1`
|
|
62
|
+
// is the row the cursor was left on regardless of a trailing newline — the same
|
|
63
|
+
// basis `buildCursorSuffix` takes as its `bottomLine`.
|
|
64
|
+
const down = previousLineCount - 1 - previousCursorPosition.y;
|
|
65
|
+
return (down > 0 ? ansiEscapes.cursorDown(down) : "") + ansiEscapes.cursorTo(0);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type CursorOnlyInput = {
|
|
69
|
+
cursorWasShown: boolean;
|
|
70
|
+
previousLineCount: number;
|
|
71
|
+
previousCursorPosition: CursorPosition | undefined;
|
|
72
|
+
cursorPosition: CursorPosition | undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
Build the escape sequence for cursor-only updates (output unchanged, cursor moved).
|
|
77
|
+
Hides cursor if it was previously shown, returns to bottom, then repositions.
|
|
78
|
+
|
|
79
|
+
`buildReturnToBottom` has just placed the cursor on row `previousLineCount - 1`, so the
|
|
80
|
+
suffix measures from there rather than recomputing the row from the output.
|
|
81
|
+
*/
|
|
82
|
+
export const buildCursorOnlySequence = (input: CursorOnlyInput): string => {
|
|
83
|
+
const hidePrefix = input.cursorWasShown ? hideCursorEscape : "";
|
|
84
|
+
const returnToBottom = buildReturnToBottom(input.previousLineCount, input.previousCursorPosition);
|
|
85
|
+
const cursorSuffix = buildCursorSuffix(input.previousLineCount - 1, input.cursorPosition);
|
|
86
|
+
return hidePrefix + returnToBottom + cursorSuffix;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
Build the prefix that hides cursor and returns to bottom before erasing or rewriting.
|
|
91
|
+
Returns empty string if cursor was not shown.
|
|
92
|
+
*/
|
|
93
|
+
export const buildReturnToBottomPrefix = (
|
|
94
|
+
cursorWasShown: boolean,
|
|
95
|
+
previousLineCount: number,
|
|
96
|
+
previousCursorPosition: CursorPosition | undefined,
|
|
97
|
+
): string => {
|
|
98
|
+
if (!cursorWasShown) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return hideCursorEscape + buildReturnToBottom(previousLineCount, previousCursorPosition);
|
|
103
|
+
};
|
package/src/devtools.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { setTimeout } from "node:timers";
|
|
2
|
+
|
|
3
|
+
// Node 22.4+ ships a stable browser-compatible WebSocket client, so no `ws`
|
|
4
|
+
// dependency is needed. react-devtools-core resolves the constructor via the
|
|
5
|
+
// `window`/`ws` globals defined here.
|
|
6
|
+
Object.defineProperties(globalThis, {
|
|
7
|
+
ws: { value: WebSocket },
|
|
8
|
+
window: { value: globalThis },
|
|
9
|
+
self: { value: globalThis },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Filter out Ink's internal components from devtools for a cleaner view.
|
|
13
|
+
// Also, since `react-devtools-shared` package isn't published on npm, we can't
|
|
14
|
+
// use its types, that's why there are hard-coded values in `type` fields below.
|
|
15
|
+
// See https://github.com/facebook/react/blob/edf6eac8a181860fd8a2d076a43806f1237495a1/packages/react-devtools-shared/src/types.js#L24
|
|
16
|
+
Object.defineProperty(globalThis, "__REACT_DEVTOOLS_COMPONENT_FILTERS__", {
|
|
17
|
+
value: [
|
|
18
|
+
{
|
|
19
|
+
// ComponentFilterElementType
|
|
20
|
+
type: 1,
|
|
21
|
+
// ElementTypeHostComponent
|
|
22
|
+
value: 7,
|
|
23
|
+
isEnabled: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
// ComponentFilterDisplayName
|
|
27
|
+
type: 2,
|
|
28
|
+
value: "InternalApp",
|
|
29
|
+
isEnabled: true,
|
|
30
|
+
isValid: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// ComponentFilterDisplayName
|
|
34
|
+
type: 2,
|
|
35
|
+
value: "InternalAppContext",
|
|
36
|
+
isEnabled: true,
|
|
37
|
+
isValid: true,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
// ComponentFilterDisplayName
|
|
41
|
+
type: 2,
|
|
42
|
+
value: "InternalStdoutContext",
|
|
43
|
+
isEnabled: true,
|
|
44
|
+
isValid: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
// ComponentFilterDisplayName
|
|
48
|
+
type: 2,
|
|
49
|
+
value: "InternalStderrContext",
|
|
50
|
+
isEnabled: true,
|
|
51
|
+
isValid: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// ComponentFilterDisplayName
|
|
55
|
+
type: 2,
|
|
56
|
+
value: "InternalStdinContext",
|
|
57
|
+
isEnabled: true,
|
|
58
|
+
isValid: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
// ComponentFilterDisplayName
|
|
62
|
+
type: 2,
|
|
63
|
+
value: "InternalFocusContext",
|
|
64
|
+
isEnabled: true,
|
|
65
|
+
isValid: true,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const isDevToolsReachable = () =>
|
|
71
|
+
new Promise<boolean>((resolve) => {
|
|
72
|
+
const socket = new WebSocket("ws://localhost:8097");
|
|
73
|
+
|
|
74
|
+
const settle = (reachable: boolean) => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
socket.close();
|
|
77
|
+
resolve(reachable);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
settle(false);
|
|
82
|
+
}, 2000);
|
|
83
|
+
// Don't let the timeout keep the process alive on its own
|
|
84
|
+
timeout.unref();
|
|
85
|
+
|
|
86
|
+
socket.addEventListener("open", () => {
|
|
87
|
+
settle(true);
|
|
88
|
+
});
|
|
89
|
+
socket.addEventListener("error", () => {
|
|
90
|
+
settle(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (await isDevToolsReachable()) {
|
|
95
|
+
// @ts-expect-error: react-devtools-core is not typed
|
|
96
|
+
const devtools = await import("react-devtools-core");
|
|
97
|
+
devtools.initialize();
|
|
98
|
+
devtools.connectToDevTools();
|
|
99
|
+
} else {
|
|
100
|
+
console.warn(
|
|
101
|
+
"SIGIL_DEV is set to true, but the React DevTools server is not running. Start it with:\n\n$ npx react-devtools\n",
|
|
102
|
+
);
|
|
103
|
+
}
|