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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +499 -313
  2. package/THIRD_PARTY_NOTICES.md +98 -23
  3. package/dist/Text-DV9CuzAT.d.ts +452 -0
  4. package/dist/ansi.d.ts +217 -0
  5. package/dist/ansi.js +87 -0
  6. package/dist/capabilities.d.ts +5 -0
  7. package/dist/capabilities.js +3 -0
  8. package/dist/cell-_ZVhbfl0.js +44 -0
  9. package/dist/color-CkbalRqK.js +2 -0
  10. package/dist/color-policy-BAC9-TZX.js +592 -0
  11. package/dist/color-policy-CCxuHIdD.d.ts +22 -0
  12. package/dist/color-profile-CyeHnG1T.d.ts +97 -0
  13. package/dist/color-profile-DHhQHY55.js +36 -0
  14. package/dist/color.d.ts +21 -0
  15. package/dist/color.js +3 -0
  16. package/dist/cursor-position-D2LAkRG0.d.ts +7 -0
  17. package/dist/detect-B3dL4Q11.js +374 -0
  18. package/dist/detect-Db6GbKOm.d.ts +195 -0
  19. package/dist/{devtools-QpCMm9JH.mjs → devtools-DbthxoD1.js} +23 -24
  20. package/dist/env-YVw64yZS.js +9 -0
  21. package/dist/escapes-CB_6CWOE.d.ts +72 -0
  22. package/dist/geometry-BxXOzJgo.d.ts +11 -0
  23. package/dist/index-D48vQhhe.d.ts +21 -0
  24. package/dist/index-DDVME65c.d.ts +919 -0
  25. package/dist/index.d.ts +1310 -0
  26. package/dist/index.js +3211 -0
  27. package/dist/jsx-dev-runtime.d.ts +2 -0
  28. package/dist/jsx-dev-runtime.js +227 -0
  29. package/dist/jsx-runtime-XtwIVFsg.js +255 -0
  30. package/dist/jsx-runtime.d.ts +2 -0
  31. package/dist/jsx-runtime.js +8 -0
  32. package/dist/osc-BFKKSqpg.js +71 -0
  33. package/dist/osc-Cn0fw77g.d.ts +23 -0
  34. package/dist/paint-Cx-zC_sX.d.ts +81 -0
  35. package/dist/query-BNc2B8GD.d.ts +152 -0
  36. package/dist/react-CTZ_8dwh.js +1197 -0
  37. package/dist/react.d.ts +2 -0
  38. package/dist/react.js +49 -0
  39. package/dist/router.d.ts +392 -0
  40. package/dist/router.js +711 -0
  41. package/dist/sample-Cqw1bjUL.js +445 -0
  42. package/dist/screen-CgC2WlVM.d.ts +49 -0
  43. package/dist/screen-CiPytswf.js +342 -0
  44. package/dist/screen.d.ts +5 -0
  45. package/dist/screen.js +5 -0
  46. package/dist/semantic-text-style-DIMzC7xt.js +91 -0
  47. package/dist/serialize-BTkAZgw1.js +79 -0
  48. package/dist/session-DDQ5V300.js +723 -0
  49. package/dist/sgr-BhwaWAJB.js +246 -0
  50. package/dist/store-C1P5fOUi.d.ts +72 -0
  51. package/dist/string-width-CijQwpIk.js +69 -0
  52. package/dist/strip-BvU4toXG.js +6 -0
  53. package/dist/terminal.d.ts +121 -0
  54. package/dist/terminal.js +2 -0
  55. package/dist/tokenize-AjqbvtiT.js +1242 -0
  56. package/dist/tokenize-Dx1y_l5H.d.ts +57 -0
  57. package/dist/truncate-D31fhU6i.js +562 -0
  58. package/dist/use-focus-B5npZrJU.js +18736 -0
  59. package/dist/yoga-5jKhYCJC.js +3465 -0
  60. package/dist/yoga.d.ts +2 -0
  61. package/dist/yoga.js +2 -0
  62. package/package.json +89 -29
  63. package/src/ansi/chalk.ts +138 -0
  64. package/src/ansi/cursor.ts +46 -0
  65. package/src/ansi/east-asian-width.ts +259 -0
  66. package/src/ansi/escapes.ts +120 -0
  67. package/src/ansi/graphemes.ts +8 -0
  68. package/src/ansi/hyperlink.ts +44 -0
  69. package/src/ansi/index.ts +27 -0
  70. package/src/ansi/osc.ts +77 -0
  71. package/src/ansi/sgr.ts +234 -0
  72. package/src/ansi/slice.ts +43 -0
  73. package/src/ansi/string-width.ts +121 -0
  74. package/src/ansi/strip.ts +13 -0
  75. package/src/ansi/tokenize.ts +380 -0
  76. package/src/ansi/truncate.ts +196 -0
  77. package/src/ansi/wrap.ts +765 -0
  78. package/src/ansi-tokenizer.ts +510 -0
  79. package/src/capabilities/color-policy.ts +34 -0
  80. package/src/capabilities/detect.ts +608 -0
  81. package/src/capabilities/index.ts +37 -0
  82. package/src/capabilities/query.ts +679 -0
  83. package/src/capabilities/store.ts +394 -0
  84. package/src/code-excerpt.ts +39 -0
  85. package/src/color/index.ts +3 -0
  86. package/src/color/paint.ts +169 -0
  87. package/src/color/palette.ts +48 -0
  88. package/src/color/sample.ts +323 -0
  89. package/src/color.ts +1 -0
  90. package/src/components/AccessibilityContext.ts +5 -0
  91. package/src/components/AnimationContext.ts +24 -0
  92. package/src/components/AnsiText.tsx +42 -0
  93. package/src/components/App.tsx +878 -0
  94. package/src/components/AppContext.ts +111 -0
  95. package/src/components/BackgroundContext.ts +7 -0
  96. package/src/components/Box.tsx +100 -0
  97. package/src/components/CursorContext.ts +19 -0
  98. package/src/components/ErrorBoundary.tsx +39 -0
  99. package/src/components/ErrorOverview.tsx +135 -0
  100. package/src/components/FocusContext.ts +30 -0
  101. package/src/components/Hyperlink.tsx +56 -0
  102. package/src/components/Newline.tsx +16 -0
  103. package/src/components/Spacer.tsx +11 -0
  104. package/src/components/Static.tsx +60 -0
  105. package/src/components/StderrContext.ts +24 -0
  106. package/src/components/StdinContext.ts +48 -0
  107. package/src/components/StdoutContext.ts +26 -0
  108. package/src/components/TerminalOscContext.ts +25 -0
  109. package/src/components/Text.tsx +122 -0
  110. package/src/components/Transform.tsx +38 -0
  111. package/src/components/VirtualList.tsx +128 -0
  112. package/src/cursor-position.ts +103 -0
  113. package/src/devtools.ts +103 -0
  114. package/src/dom.ts +301 -0
  115. package/src/env.ts +12 -0
  116. package/src/get-max-width.ts +11 -0
  117. package/src/global.d.ts +38 -0
  118. package/src/glyphs.ts +99 -0
  119. package/src/hooks/use-animation.ts +142 -0
  120. package/src/hooks/use-app.ts +8 -0
  121. package/src/hooks/use-box-metrics.ts +134 -0
  122. package/src/hooks/use-capabilities.ts +73 -0
  123. package/src/hooks/use-cursor.ts +33 -0
  124. package/src/hooks/use-focus-manager.ts +62 -0
  125. package/src/hooks/use-focus.ts +82 -0
  126. package/src/hooks/use-input.ts +267 -0
  127. package/src/hooks/use-is-screen-reader-enabled.ts +12 -0
  128. package/src/hooks/use-paste.ts +78 -0
  129. package/src/hooks/use-stderr.ts +8 -0
  130. package/src/hooks/use-stdin.ts +10 -0
  131. package/src/hooks/use-stdout.ts +8 -0
  132. package/src/hooks/use-terminal-osc.ts +59 -0
  133. package/src/hooks/use-virtual-scroll.ts +84 -0
  134. package/src/hooks/use-window-size.ts +37 -0
  135. package/src/index.ts +96 -0
  136. package/src/ink.tsx +1452 -0
  137. package/src/input-parser.ts +303 -0
  138. package/src/instances.ts +9 -0
  139. package/src/jsx-dev-runtime.ts +3 -0
  140. package/src/jsx-runtime.ts +6 -0
  141. package/src/kitty-keyboard.ts +185 -0
  142. package/src/measure-element.ts +62 -0
  143. package/src/measure-text.ts +31 -0
  144. package/src/paint-tree.ts +220 -0
  145. package/src/parse-keypress.ts +515 -0
  146. package/src/parse-stack-line.ts +138 -0
  147. package/src/patch-console.ts +106 -0
  148. package/src/quick-lru.ts +85 -0
  149. package/src/react.ts +53 -0
  150. package/src/reconciler.ts +476 -0
  151. package/src/render-background.ts +59 -0
  152. package/src/render-border.ts +167 -0
  153. package/src/render-frame.ts +83 -0
  154. package/src/render-to-string.ts +146 -0
  155. package/src/render.ts +284 -0
  156. package/src/router/components.tsx +343 -0
  157. package/src/router/context.ts +41 -0
  158. package/src/router/history.ts +194 -0
  159. package/src/router/hooks.tsx +391 -0
  160. package/src/router/index.ts +34 -0
  161. package/src/router/matcher.ts +571 -0
  162. package/src/sanitize-ansi.ts +33 -0
  163. package/src/screen/ansi.ts +184 -0
  164. package/src/screen/canvas.ts +160 -0
  165. package/src/screen/cell.ts +138 -0
  166. package/src/screen/color-profile.ts +47 -0
  167. package/src/screen/geometry.ts +9 -0
  168. package/src/screen/index.ts +6 -0
  169. package/src/screen/screen.ts +305 -0
  170. package/src/screen/serialize.ts +129 -0
  171. package/src/screen.ts +1 -0
  172. package/src/semantic-text-style.ts +118 -0
  173. package/src/signal-exit.ts +106 -0
  174. package/src/squash-text-nodes.ts +37 -0
  175. package/src/stream.ts +28 -0
  176. package/src/structured-text.ts +325 -0
  177. package/src/styles.ts +753 -0
  178. package/src/terminal/index.ts +2 -0
  179. package/src/terminal/inline-presenter.ts +120 -0
  180. package/src/terminal/input.ts +92 -0
  181. package/src/terminal/render-scheduler.ts +37 -0
  182. package/src/terminal/screen-presenter.ts +242 -0
  183. package/src/terminal/session.ts +408 -0
  184. package/src/terminal-size.ts +58 -0
  185. package/src/terminal.ts +1 -0
  186. package/src/testing/browser.ts +588 -0
  187. package/src/testing/emulators.ts +205 -0
  188. package/src/testing/explorer-app/index.html +12 -0
  189. package/src/testing/explorer-app/main.ts +381 -0
  190. package/src/testing/explorer-app/style.css +194 -0
  191. package/src/testing/explorer-app/tsconfig.json +15 -0
  192. package/src/testing/explorer-app/vite-env.d.ts +1 -0
  193. package/src/testing/index.ts +26 -0
  194. package/src/testing/keys.ts +56 -0
  195. package/src/testing/live.ts +85 -0
  196. package/src/testing/matchers.ts +70 -0
  197. package/src/testing/public.ts +94 -0
  198. package/src/testing/terminal.ts +361 -0
  199. package/src/testing/vitest.ts +157 -0
  200. package/src/throttle.ts +73 -0
  201. package/src/transform-adapter.ts +14 -0
  202. package/src/types.ts +10 -0
  203. package/src/virtual-scroll.ts +133 -0
  204. package/src/wrap-text.ts +54 -0
  205. package/src/yoga/config.ts +57 -0
  206. package/src/yoga/core/absoluteLayout.ts +626 -0
  207. package/src/yoga/core/baseline.ts +66 -0
  208. package/src/yoga/core/cache.ts +136 -0
  209. package/src/yoga/core/calculateLayout.ts +2926 -0
  210. package/src/yoga/core/config.ts +104 -0
  211. package/src/yoga/core/flexLine.ts +177 -0
  212. package/src/yoga/core/helpers.ts +293 -0
  213. package/src/yoga/core/layoutResults.ts +167 -0
  214. package/src/yoga/core/node.ts +611 -0
  215. package/src/yoga/core/numeric.ts +44 -0
  216. package/src/yoga/core/pixelGrid.ts +151 -0
  217. package/src/yoga/core/style.ts +887 -0
  218. package/src/yoga/core/types.ts +224 -0
  219. package/src/yoga/generated/YGEnums.ts +263 -0
  220. package/src/yoga/index.ts +19 -0
  221. package/src/yoga/node.ts +1140 -0
  222. package/dist/index.d.mts +0 -2379
  223. package/dist/index.mjs +0 -10072
@@ -0,0 +1,134 @@
1
+ import { type RefObject, useState, useEffect, useCallback, useMemo } from "react";
2
+
3
+ import { type DOMElement, addLayoutListener } from "#/dom.ts";
4
+
5
+ // Yoga's `right`/`bottom` are omitted: always `0` for flow layout and unintuitive for absolute positioning.
6
+ /**
7
+ Metrics of a box element.
8
+
9
+ All positions are relative to the element's parent.
10
+ */
11
+ export type BoxMetrics = {
12
+ /**
13
+ Element width.
14
+ */
15
+ readonly width: number;
16
+
17
+ /**
18
+ Element height.
19
+ */
20
+ readonly height: number;
21
+
22
+ /**
23
+ Distance from the left edge of the parent.
24
+ */
25
+ readonly left: number;
26
+
27
+ /**
28
+ Distance from the top edge of the parent.
29
+ */
30
+ readonly top: number;
31
+ };
32
+
33
+ export type UseBoxMetricsResult = BoxMetrics & {
34
+ /**
35
+ Whether the currently tracked element has been measured in the latest layout pass.
36
+ */
37
+ readonly hasMeasured: boolean;
38
+ };
39
+
40
+ const emptyMetrics: BoxMetrics = {
41
+ width: 0,
42
+ height: 0,
43
+ left: 0,
44
+ top: 0,
45
+ };
46
+
47
+ // eslint-disable-next-line @typescript-eslint/no-restricted-types
48
+ const findRootNode = (node: DOMElement | null): DOMElement | undefined => {
49
+ if (!node) {
50
+ return;
51
+ }
52
+
53
+ if (!node.parentNode) {
54
+ return node.nodeName === "ink-root" ? node : undefined;
55
+ }
56
+
57
+ return findRootNode(node.parentNode);
58
+ };
59
+
60
+ /**
61
+ A React hook that returns the current layout metrics for a tracked box element.
62
+ It updates when layout changes (for example terminal resize, sibling/content changes, or position changes).
63
+
64
+ The hook returns `{width: 0, height: 0, left: 0, top: 0}` until the first layout pass completes. It also returns zeros when the tracked ref is detached.
65
+
66
+ Use `hasMeasured` to detect when the currently tracked element has been measured.
67
+
68
+ @example
69
+ ```tsx
70
+ import {useRef} from 'react';
71
+ import {Box, Text, useBoxMetrics} from 'ink';
72
+
73
+ const Example = () => {
74
+ const ref = useRef(null);
75
+ const {width, height, left, top, hasMeasured} = useBoxMetrics(ref);
76
+ return (
77
+ <Box ref={ref}>
78
+ <Text>
79
+ {hasMeasured ? `${width}x${height} at ${left},${top}` : 'Measuring...'}
80
+ </Text>
81
+ </Box>
82
+ );
83
+ };
84
+ ```
85
+ */
86
+ export const useBoxMetrics = (
87
+ /* eslint-disable-next-line @typescript-eslint/no-restricted-types --
88
+ Creating a ref object with an initial null, especially when the ref object
89
+ will be passed to a DOM node's ref attribute, is common in React. */
90
+ ref: RefObject<DOMElement | null>,
91
+ ): UseBoxMetricsResult => {
92
+ const [metrics, setMetrics] = useState(emptyMetrics);
93
+ const [hasMeasured, setHasMeasured] = useState(false);
94
+
95
+ const updateMetrics = useCallback(() => {
96
+ const layout = ref.current?.yogaNode?.getComputedLayout() ?? emptyMetrics;
97
+
98
+ setMetrics((previousMetrics) => {
99
+ const hasChanged =
100
+ previousMetrics.width !== layout.width ||
101
+ previousMetrics.height !== layout.height ||
102
+ previousMetrics.left !== layout.left ||
103
+ previousMetrics.top !== layout.top;
104
+
105
+ return hasChanged ? layout : previousMetrics;
106
+ });
107
+
108
+ setHasMeasured(Boolean(ref.current));
109
+ }, [ref]);
110
+
111
+ // Runs after every render of this component.
112
+ // This keeps metrics fresh when local state/props in this subtree change.
113
+ useEffect(updateMetrics);
114
+
115
+ // Subscribe to root layout commits so memoized components still receive
116
+ // sibling-driven position/size updates, even when they skip re-rendering.
117
+ useEffect(() => {
118
+ const rootNode = findRootNode(ref.current);
119
+
120
+ if (!rootNode) {
121
+ return;
122
+ }
123
+
124
+ return addLayoutListener(rootNode, updateMetrics);
125
+ });
126
+
127
+ return useMemo(
128
+ () => ({
129
+ ...metrics,
130
+ hasMeasured,
131
+ }),
132
+ [metrics, hasMeasured],
133
+ );
134
+ };
@@ -0,0 +1,73 @@
1
+ import { useEffect, useEffectEvent, useSyncExternalStore } from "react";
2
+
3
+ import { type Capabilities } from "#/capabilities/detect.ts";
4
+ import { getCapabilities } from "#/capabilities/store.ts";
5
+ import { useStdinContext } from "#/hooks/use-stdin.ts";
6
+ import { useStdout } from "#/hooks/use-stdout.ts";
7
+
8
+ /**
9
+ Returns everything knowable about the terminal: size, identity, platform,
10
+ color depth, theme, and feature support.
11
+
12
+ A thin wrapper over the framework-free capabilities store (`getCapabilities`):
13
+ environment-derived facts are available immediately; facts only the terminal
14
+ itself can answer fill in after a lazy one-time query, and re-mounting
15
+ consumers refreshes the dynamic facts (theme colors, pixel geometry).
16
+ Re-renders on terminal resize and whenever query answers arrive.
17
+ */
18
+ export const useCapabilities = (): Capabilities => {
19
+ const { stdout } = useStdout();
20
+ const { stdin } = useStdinContext();
21
+ const store = getCapabilities(stdin, stdout);
22
+
23
+ const capabilities = useSyncExternalStore(store.subscribe, () => store.current);
24
+
25
+ useEffect(() => {
26
+ void store.query();
27
+ }, [store]);
28
+
29
+ return capabilities;
30
+ };
31
+
32
+ /**
33
+ Calls `onChange` whenever the terminal changes: resizes (including in-band
34
+ pixel geometry), color scheme switches, window focus, and query answers
35
+ arriving. The React wrapper over `capabilities.subscribe()` for side effects —
36
+ for rendering, use `useCapabilities` instead.
37
+
38
+ The callback always sees the latest render's closure and changing it does not
39
+ resubscribe. Both the new and previous snapshot are passed, so handlers can
40
+ react to the specific change:
41
+
42
+ ```tsx
43
+ useCapabilitiesChange((next, previous) => {
44
+ if (next.theme.appearance !== previous.theme.appearance) {
45
+ // re-theme
46
+ }
47
+ });
48
+ ```
49
+ */
50
+ export const useCapabilitiesChange = (
51
+ onChange: (capabilities: Capabilities, previous: Capabilities) => void,
52
+ ): void => {
53
+ const { stdout } = useStdout();
54
+ const { stdin } = useStdinContext();
55
+ const store = getCapabilities(stdin, stdout);
56
+
57
+ const handleChange = useEffectEvent(onChange);
58
+
59
+ useEffect(() => {
60
+ let previous = store.current;
61
+ const unsubscribe = store.subscribe((next) => {
62
+ const before = previous;
63
+ previous = next;
64
+ handleChange(next, before);
65
+ });
66
+
67
+ // Make sure the terminal has been asked — push report modes only turn
68
+ // on once the query has revealed support for them.
69
+ void store.query();
70
+
71
+ return unsubscribe;
72
+ }, [store]);
73
+ };
@@ -0,0 +1,33 @@
1
+ import { useContext, useRef, useCallback, useInsertionEffect } from "react";
2
+
3
+ import { CursorContext } from "#/components/CursorContext.ts";
4
+ import { type CursorPosition } from "#/cursor-position.ts";
5
+
6
+ /**
7
+ A React hook that returns methods to control the terminal cursor position.
8
+
9
+ Setting a cursor position makes the cursor visible at the specified coordinates (relative to the Ink output origin). This is useful for IME (Input Method Editor) support, where the composing character is displayed at the cursor location.
10
+
11
+ Pass `undefined` to hide the cursor.
12
+ */
13
+ export const useCursor = () => {
14
+ const context = useContext(CursorContext);
15
+ const positionRef = useRef<CursorPosition | undefined>(undefined);
16
+
17
+ const setCursorPosition = useCallback((position: CursorPosition | undefined) => {
18
+ positionRef.current = position;
19
+ }, []);
20
+
21
+ // Propagate cursor position to the terminal presenter only during commit.
22
+ // useInsertionEffect runs before resetAfterCommit (which triggers onRender),
23
+ // and does NOT run for abandoned concurrent renders (e.g. suspended components).
24
+ // This prevents cursor state from leaking across render boundaries.
25
+ useInsertionEffect(() => {
26
+ context.setCursorPosition(positionRef.current);
27
+ return () => {
28
+ context.setCursorPosition(undefined);
29
+ };
30
+ });
31
+
32
+ return { setCursorPosition };
33
+ };
@@ -0,0 +1,62 @@
1
+ import { useContext } from "react";
2
+
3
+ import { FocusContext, type Props } from "#/components/FocusContext.ts";
4
+
5
+ type Output = {
6
+ /**
7
+ Enable focus management for all components.
8
+ */
9
+ enableFocus: Props["enableFocus"];
10
+
11
+ /**
12
+ Disable focus management for all components. The currently active component (if there's one) will lose its focus.
13
+ */
14
+ disableFocus: Props["disableFocus"];
15
+
16
+ /**
17
+ Switch focus to the next focusable component. If there's no active component right now, focus will be given to the first focusable component. If the active component is the last in the list of focusable components, focus will be switched to the first focusable component.
18
+ */
19
+ focusNext: Props["focusNext"];
20
+
21
+ /**
22
+ Switch focus to the previous focusable component. If there's no active component right now, focus will be given to the first focusable component. If the active component is the first in the list of focusable components, focus will be switched to the last focusable component.
23
+ */
24
+ focusPrevious: Props["focusPrevious"];
25
+
26
+ /**
27
+ Switch focus to the element with provided `id`. If there's no element with that `id`, focus is not changed.
28
+ */
29
+ focus: Props["focus"];
30
+
31
+ /**
32
+ The ID of the currently focused component, or `undefined` if no component is focused.
33
+
34
+ @example
35
+ ```tsx
36
+ import {Text, useFocusManager} from 'ink';
37
+
38
+ const Example = () => {
39
+ const {activeId} = useFocusManager();
40
+
41
+ return <Text>Focused: {activeId ?? 'none'}</Text>;
42
+ };
43
+ ```
44
+ */
45
+ activeId: Props["activeId"];
46
+ };
47
+
48
+ /**
49
+ A React hook that returns methods to enable or disable focus management for all components or manually switch focus to the next or previous components.
50
+ */
51
+ export const useFocusManager = (): Output => {
52
+ const focusContext = useContext(FocusContext);
53
+
54
+ return {
55
+ enableFocus: focusContext.enableFocus,
56
+ disableFocus: focusContext.disableFocus,
57
+ focusNext: focusContext.focusNext,
58
+ focusPrevious: focusContext.focusPrevious,
59
+ focus: focusContext.focus,
60
+ activeId: focusContext.activeId,
61
+ };
62
+ };
@@ -0,0 +1,82 @@
1
+ import { useEffect, useContext, useId } from "react";
2
+
3
+ import { FocusContext } from "#/components/FocusContext.ts";
4
+ import { useStdin } from "#/hooks/use-stdin.ts";
5
+
6
+ type Input = {
7
+ /**
8
+ Enable or disable this component's focus, while still maintaining its position in the list of focusable components.
9
+ */
10
+ isActive?: boolean;
11
+
12
+ /**
13
+ Auto-focus this component if there's no active (focused) component right now.
14
+ */
15
+ autoFocus?: boolean;
16
+
17
+ /**
18
+ Assign an ID to this component, so it can be programmatically focused with `focus(id)`.
19
+ */
20
+ id?: string;
21
+ };
22
+
23
+ type Output = {
24
+ /**
25
+ Determines whether this component is focused.
26
+ */
27
+ isFocused: boolean;
28
+
29
+ /**
30
+ Allows focusing a specific element with the provided `id`.
31
+ */
32
+ focus: (id: string) => void;
33
+ };
34
+
35
+ /**
36
+ A React hook that returns focus state and focus controls for the current component.
37
+ A component that uses the `useFocus` hook becomes "focusable" to Ink, so when the user presses <kbd>Tab</kbd>, Ink will switch focus to this component. If there are multiple components that execute the `useFocus` hook, focus will be given to them in the order in which these components are rendered.
38
+ */
39
+ export const useFocus = ({
40
+ isActive = true,
41
+ autoFocus = false,
42
+ id: customId,
43
+ }: Input = {}): Output => {
44
+ const { isRawModeSupported, setRawMode } = useStdin();
45
+ const { activeId, add, remove, activate, deactivate, focus } = useContext(FocusContext);
46
+
47
+ const autoId = useId();
48
+ const id = customId ?? autoId;
49
+
50
+ useEffect(() => {
51
+ add(id, { autoFocus });
52
+
53
+ return () => {
54
+ remove(id);
55
+ };
56
+ }, [id, autoFocus, add, remove]);
57
+
58
+ useEffect(() => {
59
+ if (isActive) {
60
+ activate(id);
61
+ } else {
62
+ deactivate(id);
63
+ }
64
+ }, [isActive, id, activate, deactivate]);
65
+
66
+ useEffect(() => {
67
+ if (!isRawModeSupported || !isActive) {
68
+ return;
69
+ }
70
+
71
+ setRawMode(true);
72
+
73
+ return () => {
74
+ setRawMode(false);
75
+ };
76
+ }, [isActive, isRawModeSupported, setRawMode]);
77
+
78
+ return {
79
+ isFocused: Boolean(id) && activeId === id,
80
+ focus,
81
+ };
82
+ };
@@ -0,0 +1,267 @@
1
+ import { useEffect, useEffectEvent } from "react";
2
+
3
+ import { ESC } from "#/ansi/escapes.ts";
4
+ import { useStdinContext } from "#/hooks/use-stdin.ts";
5
+ import { parseKeypress, nonAlphanumericKeys } from "#/parse-keypress.ts";
6
+ import { reconciler } from "#/reconciler.ts";
7
+
8
+ /**
9
+ Handy information about a key that was pressed.
10
+ */
11
+ export type Key = {
12
+ /**
13
+ Up arrow key was pressed.
14
+ */
15
+ upArrow: boolean;
16
+
17
+ /**
18
+ Down arrow key was pressed.
19
+ */
20
+ downArrow: boolean;
21
+
22
+ /**
23
+ Left arrow key was pressed.
24
+ */
25
+ leftArrow: boolean;
26
+
27
+ /**
28
+ Right arrow key was pressed.
29
+ */
30
+ rightArrow: boolean;
31
+
32
+ /**
33
+ Page Down key was pressed.
34
+ */
35
+ pageDown: boolean;
36
+
37
+ /**
38
+ Page Up key was pressed.
39
+ */
40
+ pageUp: boolean;
41
+
42
+ /**
43
+ Home key was pressed.
44
+ */
45
+ home: boolean;
46
+
47
+ /**
48
+ End key was pressed.
49
+ */
50
+ end: boolean;
51
+
52
+ /**
53
+ Return (Enter) key was pressed.
54
+ */
55
+ return: boolean;
56
+
57
+ /**
58
+ Escape key was pressed.
59
+ */
60
+ escape: boolean;
61
+
62
+ /**
63
+ Ctrl key was pressed.
64
+ */
65
+ ctrl: boolean;
66
+
67
+ /**
68
+ Shift key was pressed.
69
+ */
70
+ shift: boolean;
71
+
72
+ /**
73
+ Tab key was pressed.
74
+ */
75
+ tab: boolean;
76
+
77
+ /**
78
+ Backspace key was pressed.
79
+ */
80
+ backspace: boolean;
81
+
82
+ /**
83
+ Delete key was pressed.
84
+ */
85
+ delete: boolean;
86
+
87
+ /**
88
+ [Meta key](https://en.wikipedia.org/wiki/Meta_key) was pressed.
89
+ */
90
+ meta: boolean;
91
+
92
+ /**
93
+ Super key (Cmd on Mac, Win on Windows) was pressed.
94
+
95
+ Only available with kitty keyboard protocol.
96
+ */
97
+ super: boolean;
98
+
99
+ /**
100
+ Hyper key was pressed.
101
+
102
+ Only available with kitty keyboard protocol.
103
+ */
104
+ hyper: boolean;
105
+
106
+ /**
107
+ Caps Lock is active.
108
+
109
+ Only available with kitty keyboard protocol.
110
+ */
111
+ capsLock: boolean;
112
+
113
+ /**
114
+ Num Lock is active.
115
+
116
+ Only available with kitty keyboard protocol.
117
+ */
118
+ numLock: boolean;
119
+
120
+ /**
121
+ Event type for key events.
122
+
123
+ Only available with kitty keyboard protocol.
124
+ */
125
+ eventType?: "press" | "repeat" | "release";
126
+ };
127
+
128
+ type Handler = (input: string, key: Key) => void;
129
+
130
+ type Options = {
131
+ /**
132
+ Enable or disable capturing of user input. Useful when there are multiple `useInput` hooks used at once to avoid handling the same input several times.
133
+
134
+ @default true
135
+ */
136
+ isActive?: boolean;
137
+ };
138
+
139
+ /**
140
+ A React hook that returns `void` and handles user input.
141
+ It's a more convenient alternative to using `StdinContext` and listening for `data` events. The callback you pass to `useInput` is called for each character when the user enters any input. However, if the user pastes text and it's more than one character, the callback will be called only once, and the whole string will be passed as `input`.
142
+
143
+ ```
144
+ import {useInput} from 'ink';
145
+
146
+ const UserInput = () => {
147
+ useInput((input, key) => {
148
+ if (input === 'q') {
149
+ // Exit program
150
+ }
151
+
152
+ if (key.leftArrow) {
153
+ // Left arrow key pressed
154
+ }
155
+ });
156
+
157
+ return …
158
+ };
159
+ ```
160
+ */
161
+ export const useInput = (inputHandler: Handler, options: Options = {}) => {
162
+ // eslint-disable-next-line @typescript-eslint/naming-convention
163
+ const { setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdinContext();
164
+
165
+ useEffect(() => {
166
+ if (options.isActive === false) {
167
+ return;
168
+ }
169
+
170
+ setRawMode(true);
171
+
172
+ return () => {
173
+ setRawMode(false);
174
+ };
175
+ }, [options.isActive, setRawMode]);
176
+
177
+ const handleData = useEffectEvent((data: string) => {
178
+ const keypress = parseKeypress(data);
179
+
180
+ const key: Key = {
181
+ upArrow: keypress.name === "up",
182
+ downArrow: keypress.name === "down",
183
+ leftArrow: keypress.name === "left",
184
+ rightArrow: keypress.name === "right",
185
+ pageDown: keypress.name === "pagedown",
186
+ pageUp: keypress.name === "pageup",
187
+ home: keypress.name === "home",
188
+ end: keypress.name === "end",
189
+ return: keypress.name === "return",
190
+ escape: keypress.name === "escape",
191
+ ctrl: keypress.ctrl,
192
+ shift: keypress.shift,
193
+ tab: keypress.name === "tab",
194
+ backspace: keypress.name === "backspace",
195
+ delete: keypress.name === "delete",
196
+ meta: keypress.meta,
197
+ // Kitty keyboard protocol modifiers
198
+ super: keypress.super ?? false,
199
+ hyper: keypress.hyper ?? false,
200
+ capsLock: keypress.capsLock ?? false,
201
+ numLock: keypress.numLock ?? false,
202
+ eventType: keypress.eventType,
203
+ };
204
+
205
+ let input: string;
206
+ if (keypress.isKittyProtocol) {
207
+ // Use text-as-codepoints field for printable keys (needed when
208
+ // reportAllKeysAsEscapeCodes flag is enabled), suppress non-printable
209
+ if (keypress.isPrintable) {
210
+ input = keypress.text ?? keypress.name;
211
+ } else if (keypress.ctrl && keypress.name.length === 1) {
212
+ // Ctrl+letter via codepoint 1-26 form: not printable text, but
213
+ // the letter name must flow through so handlers (e.g. exitOnCtrlC
214
+ // checking `input === 'c' && key.ctrl`) still work.
215
+ input = keypress.name;
216
+ } else {
217
+ input = "";
218
+ }
219
+ } else if (keypress.ctrl) {
220
+ // Keypress.name is guaranteed non-undefined by parseKeypress,
221
+ // but guard defensively since a TypeError here would crash the
222
+ // entire Ink app (see https://github.com/vadimdemedes/ink/issues/901).
223
+ input = keypress.name ?? "";
224
+ } else {
225
+ input = keypress.sequence;
226
+ }
227
+
228
+ if (!keypress.isKittyProtocol && nonAlphanumericKeys.includes(keypress.name)) {
229
+ input = "";
230
+ }
231
+
232
+ // Strip escape prefix from broken/incomplete sequences that
233
+ // parseKeypress did not fully resolve (e.g. a flushed "\u001B[").
234
+ if (input.startsWith(ESC)) {
235
+ input = input.slice(1);
236
+ }
237
+
238
+ if (input.length === 1 && /[A-Z]/.test(input)) {
239
+ key.shift = true;
240
+ }
241
+
242
+ // If app is supposed to exit on Ctrl+C, skip input listeners.
243
+ if (input === "c" && key.ctrl && internal_exitOnCtrlC) {
244
+ return;
245
+ }
246
+
247
+ // Use discreteUpdates to assign DiscreteEventPriority to state
248
+ // updates from keyboard input, ensuring they are processed at the
249
+ // highest priority in concurrent mode.
250
+ // @ts-expect-error Types require 5 arguments (fn, a, b, c, d) but only fn is needed at runtime.
251
+ reconciler.discreteUpdates(() => {
252
+ inputHandler(input, key);
253
+ });
254
+ });
255
+
256
+ useEffect(() => {
257
+ if (options.isActive === false) {
258
+ return;
259
+ }
260
+
261
+ internal_eventEmitter.on("input", handleData);
262
+
263
+ return () => {
264
+ internal_eventEmitter.removeListener("input", handleData);
265
+ };
266
+ }, [options.isActive, internal_eventEmitter]);
267
+ };
@@ -0,0 +1,12 @@
1
+ import { useContext } from "react";
2
+
3
+ import { accessibilityContext } from "#/components/AccessibilityContext.ts";
4
+
5
+ /**
6
+ A React hook that returns whether a screen reader is enabled.
7
+ This is useful when you want to render different output for screen readers.
8
+ */
9
+ export const useIsScreenReaderEnabled = (): boolean => {
10
+ const { isScreenReaderEnabled } = useContext(accessibilityContext);
11
+ return isScreenReaderEnabled;
12
+ };