@nuvin/ink 6.6.7 → 7.0.1-alpha
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/build/ansi-tokenizer.d.ts +38 -0
- package/build/ansi-tokenizer.d.ts.map +1 -0
- package/build/ansi-tokenizer.js +316 -0
- package/build/ansi-tokenizer.js.map +1 -0
- package/build/colorize.d.ts.map +1 -0
- package/build/components/AccessibilityContext.d.ts.map +1 -0
- package/build/components/AnimationContext.d.ts +9 -0
- package/build/components/AnimationContext.d.ts.map +1 -0
- package/build/components/AnimationContext.js +13 -0
- package/build/components/AnimationContext.js.map +1 -0
- package/build/components/App.d.ts +11 -49
- package/build/components/App.d.ts.map +1 -0
- package/build/components/App.js +412 -227
- package/build/components/App.js.map +1 -1
- package/build/components/AppContext.d.ts +33 -3
- package/build/components/AppContext.d.ts.map +1 -0
- package/build/components/AppContext.js +2 -1
- package/build/components/AppContext.js.map +1 -1
- package/build/components/BackgroundContext.d.ts.map +1 -0
- package/build/components/Box.d.ts +15 -8
- package/build/components/Box.d.ts.map +1 -0
- package/build/components/Box.js +12 -10
- package/build/components/Box.js.map +1 -1
- package/build/components/CursorContext.d.ts +11 -0
- package/build/components/CursorContext.d.ts.map +1 -0
- package/build/components/CursorContext.js +8 -0
- package/build/components/CursorContext.js.map +1 -0
- package/build/components/ErrorBoundary.d.ts +18 -0
- package/build/components/ErrorBoundary.d.ts.map +1 -0
- package/build/components/ErrorBoundary.js +23 -0
- package/build/components/ErrorBoundary.js.map +1 -0
- package/build/components/ErrorOverview.d.ts.map +1 -0
- package/build/components/ErrorOverview.js +6 -6
- package/build/components/ErrorOverview.js.map +1 -1
- package/build/components/FocusContext.d.ts.map +1 -0
- package/build/components/Newline.d.ts.map +1 -0
- package/build/components/Spacer.d.ts.map +1 -0
- package/build/components/Static.d.ts.map +1 -0
- package/build/components/Static.js.map +1 -1
- package/build/components/StderrContext.d.ts.map +1 -0
- package/build/components/StdinContext.d.ts +7 -1
- package/build/components/StdinContext.d.ts.map +1 -0
- package/build/components/StdinContext.js +1 -0
- package/build/components/StdinContext.js.map +1 -1
- package/build/components/StdoutContext.d.ts.map +1 -0
- package/build/components/Text.d.ts +1 -1
- package/build/components/Text.d.ts.map +1 -0
- package/build/components/Text.js +1 -1
- package/build/components/Text.js.map +1 -1
- package/build/components/Transform.d.ts +1 -1
- package/build/components/Transform.d.ts.map +1 -0
- package/build/cursor-helpers.d.ts +38 -0
- package/build/cursor-helpers.d.ts.map +1 -0
- package/build/cursor-helpers.js +56 -0
- package/build/cursor-helpers.js.map +1 -0
- package/build/devtools-window-polyfill.d.ts.map +1 -0
- package/build/devtools-window-polyfill.js +7 -4
- package/build/devtools-window-polyfill.js.map +1 -1
- package/build/devtools.d.ts.map +1 -0
- package/build/devtools.js +31 -6
- package/build/devtools.js.map +1 -1
- package/build/diagnostics.d.ts +5 -0
- package/build/diagnostics.d.ts.map +1 -0
- package/build/diagnostics.js +7 -0
- package/build/diagnostics.js.map +1 -0
- package/build/dom.d.ts +5 -1
- package/build/dom.d.ts.map +1 -0
- package/build/dom.js +20 -1
- package/build/dom.js.map +1 -1
- package/build/get-max-width.d.ts.map +1 -0
- package/build/hooks/use-animation.d.ts +49 -0
- package/build/hooks/use-animation.d.ts.map +1 -0
- package/build/hooks/use-animation.js +87 -0
- package/build/hooks/use-animation.js.map +1 -0
- package/build/hooks/use-app.d.ts +5 -2
- package/build/hooks/use-app.d.ts.map +1 -0
- package/build/hooks/use-app.js +1 -1
- package/build/hooks/use-box-metrics.d.ts +59 -0
- package/build/hooks/use-box-metrics.d.ts.map +1 -0
- package/build/hooks/use-box-metrics.js +88 -0
- package/build/hooks/use-box-metrics.js.map +1 -0
- package/build/hooks/use-cursor.d.ts +12 -0
- package/build/hooks/use-cursor.d.ts.map +1 -0
- package/build/hooks/use-cursor.js +29 -0
- package/build/hooks/use-cursor.js.map +1 -0
- package/build/hooks/use-focus-manager.d.ts +17 -2
- package/build/hooks/use-focus-manager.d.ts.map +1 -0
- package/build/hooks/use-focus-manager.js +2 -1
- package/build/hooks/use-focus-manager.js.map +1 -1
- package/build/hooks/use-focus.d.ts +2 -1
- package/build/hooks/use-focus.d.ts.map +1 -0
- package/build/hooks/use-focus.js +5 -4
- package/build/hooks/use-focus.js.map +1 -1
- package/build/hooks/use-input.d.ts +40 -1
- package/build/hooks/use-input.d.ts.map +1 -0
- package/build/hooks/use-input.js +65 -25
- package/build/hooks/use-input.js.map +1 -1
- package/build/hooks/use-is-screen-reader-enabled.d.ts +2 -1
- package/build/hooks/use-is-screen-reader-enabled.d.ts.map +1 -0
- package/build/hooks/use-is-screen-reader-enabled.js +2 -1
- package/build/hooks/use-is-screen-reader-enabled.js.map +1 -1
- package/build/hooks/use-paste.d.ts +35 -0
- package/build/hooks/use-paste.d.ts.map +1 -0
- package/build/hooks/use-paste.js +66 -0
- package/build/hooks/use-paste.js.map +1 -0
- package/build/hooks/use-stderr.d.ts +1 -1
- package/build/hooks/use-stderr.d.ts.map +1 -0
- package/build/hooks/use-stderr.js +1 -1
- package/build/hooks/use-stdin.d.ts +4 -2
- package/build/hooks/use-stdin.d.ts.map +1 -0
- package/build/hooks/use-stdin.js +2 -1
- package/build/hooks/use-stdin.js.map +1 -1
- package/build/hooks/use-stdout.d.ts +1 -1
- package/build/hooks/use-stdout.d.ts.map +1 -0
- package/build/hooks/use-stdout.js +1 -1
- package/build/hooks/use-window-size.d.ts +18 -0
- package/build/hooks/use-window-size.d.ts.map +1 -0
- package/build/hooks/use-window-size.js +22 -0
- package/build/hooks/use-window-size.js.map +1 -0
- package/build/index.d.ts +16 -1
- package/build/index.d.ts.map +1 -0
- package/build/index.js +8 -0
- package/build/index.js.map +1 -1
- package/build/ink.d.ts +94 -5
- package/build/ink.d.ts.map +1 -0
- package/build/ink.js +586 -81
- package/build/ink.js.map +1 -1
- package/build/input-parser.d.ts +10 -0
- package/build/input-parser.d.ts.map +1 -0
- package/build/input-parser.js +194 -0
- package/build/input-parser.js.map +1 -0
- package/build/instances.d.ts.map +1 -0
- package/build/kitty-keyboard.d.ts +23 -0
- package/build/kitty-keyboard.d.ts.map +1 -0
- package/build/kitty-keyboard.js +32 -0
- package/build/kitty-keyboard.js.map +1 -0
- package/build/log-update.d.ts +7 -1
- package/build/log-update.d.ts.map +1 -0
- package/build/log-update.js +181 -56
- package/build/log-update.js.map +1 -1
- package/build/measure-element.d.ts +15 -3
- package/build/measure-element.d.ts.map +1 -0
- package/build/measure-element.js +31 -5
- package/build/measure-element.js.map +1 -1
- package/build/measure-text.d.ts +2 -0
- package/build/measure-text.d.ts.map +1 -0
- package/build/measure-text.js +13 -11
- package/build/measure-text.js.map +1 -1
- package/build/output.d.ts +2 -1
- package/build/output.d.ts.map +1 -0
- package/build/output.js +62 -58
- package/build/output.js.map +1 -1
- package/build/parse-keypress.d.ts +9 -3
- package/build/parse-keypress.d.ts.map +1 -0
- package/build/parse-keypress.js +283 -13
- package/build/parse-keypress.js.map +1 -1
- package/build/reconciler.d.ts.map +1 -0
- package/build/reconciler.js +57 -37
- package/build/reconciler.js.map +1 -1
- package/build/render-background.d.ts.map +1 -0
- package/build/render-background.js +2 -0
- package/build/render-background.js.map +1 -1
- package/build/render-border.d.ts +1 -1
- package/build/render-border.d.ts.map +1 -0
- package/build/render-border.js +35 -24
- package/build/render-border.js.map +1 -1
- package/build/render-node-to-output.d.ts +1 -1
- package/build/render-node-to-output.d.ts.map +1 -0
- package/build/render-node-to-output.js +35 -18
- package/build/render-node-to-output.js.map +1 -1
- package/build/render-to-string.d.ts +38 -0
- package/build/render-to-string.d.ts.map +1 -0
- package/build/render-to-string.js +116 -0
- package/build/render-to-string.js.map +1 -0
- package/build/render.d.ts +91 -3
- package/build/render.d.ts.map +1 -0
- package/build/render.js +17 -5
- package/build/render.js.map +1 -1
- package/build/renderer.d.ts.map +1 -0
- package/build/sanitize-ansi.d.ts +2 -0
- package/build/sanitize-ansi.d.ts.map +1 -0
- package/build/sanitize-ansi.js +27 -0
- package/build/sanitize-ansi.js.map +1 -0
- package/build/squash-text-nodes.d.ts.map +1 -0
- package/build/squash-text-nodes.js +18 -1
- package/build/squash-text-nodes.js.map +1 -1
- package/build/styles.d.ts +75 -37
- package/build/styles.d.ts.map +1 -0
- package/build/styles.js +87 -44
- package/build/styles.js.map +1 -1
- package/build/utils.d.ts +9 -2
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +18 -3
- package/build/utils.js.map +1 -1
- package/build/wrap-text.d.ts +2 -0
- package/build/wrap-text.d.ts.map +1 -0
- package/build/wrap-text.js +21 -18
- package/build/wrap-text.js.map +1 -1
- package/build/write-synchronized.d.ts +4 -0
- package/build/write-synchronized.d.ts.map +1 -0
- package/build/write-synchronized.js +9 -0
- package/build/write-synchronized.js.map +1 -0
- package/package.json +46 -106
- package/readme.md +850 -64
- package/build/components/FixedLayout.d.ts +0 -12
- package/build/components/FixedLayout.js +0 -19
- package/build/components/FixedLayout.js.map +0 -1
- package/build/components/VirtualizedList.d.ts +0 -21
- package/build/components/VirtualizedList.js +0 -55
- package/build/components/VirtualizedList.js.map +0 -1
- package/build/hooks/use-mouse.d.ts +0 -15
- package/build/hooks/use-mouse.js +0 -121
- package/build/hooks/use-mouse.js.map +0 -1
- package/build/hooks/use-virtualization.d.ts +0 -19
- package/build/hooks/use-virtualization.js +0 -56
- package/build/hooks/use-virtualization.js.map +0 -1
package/build/components/App.js
CHANGED
|
@@ -1,109 +1,198 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import process from 'node:process';
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useState, useRef, useCallback, useMemo, useEffect, } from 'react';
|
|
4
4
|
import cliCursor from 'cli-cursor';
|
|
5
|
+
import { createInputParser } from '../input-parser.js';
|
|
5
6
|
import AppContext from './AppContext.js';
|
|
6
7
|
import StdinContext from './StdinContext.js';
|
|
7
8
|
import StdoutContext from './StdoutContext.js';
|
|
8
9
|
import StderrContext from './StderrContext.js';
|
|
9
10
|
import FocusContext from './FocusContext.js';
|
|
10
|
-
import
|
|
11
|
+
import AnimationContext from './AnimationContext.js';
|
|
12
|
+
import CursorContext from './CursorContext.js';
|
|
13
|
+
import ErrorBoundary from './ErrorBoundary.js';
|
|
11
14
|
const tab = '\t';
|
|
12
15
|
const shiftTab = '\u001B[Z';
|
|
13
16
|
const escape = '\u001B';
|
|
14
17
|
// Root component for all Ink apps
|
|
15
18
|
// It renders stdin and stdout contexts, so that children can access them if needed
|
|
16
19
|
// It also handles Ctrl+C exiting and cursor visibility
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
20
|
+
function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, exitOnCtrlC, onExit, onWaitUntilRenderFlush, setCursorPosition, interactive, renderThrottleMs, }) {
|
|
21
|
+
const [isFocusEnabled, setIsFocusEnabled] = useState(true);
|
|
22
|
+
const [activeFocusId, setActiveFocusId] = useState(undefined);
|
|
23
|
+
// Focusables array is managed internally via setFocusables callback pattern
|
|
24
|
+
// eslint-disable-next-line react/hook-use-state
|
|
25
|
+
const [, setFocusables] = useState([]);
|
|
26
|
+
// Track focusables count for tab navigation check (avoids stale closure)
|
|
27
|
+
const focusablesCountRef = useRef(0);
|
|
28
|
+
const animationSubscribersRef = useRef(new Map());
|
|
29
|
+
const animationTimerRef = useRef(undefined);
|
|
28
30
|
// Count how many components enabled raw mode to avoid disabling
|
|
29
31
|
// raw mode until all components don't need it anymore
|
|
30
|
-
rawModeEnabledCount = 0;
|
|
32
|
+
const rawModeEnabledCount = useRef(0);
|
|
33
|
+
// Count how many components enabled bracketed paste mode
|
|
34
|
+
const bracketedPasteModeEnabledCount = useRef(0);
|
|
31
35
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
32
|
-
internal_eventEmitter = new EventEmitter();
|
|
36
|
+
const internal_eventEmitter = useRef(new EventEmitter());
|
|
37
|
+
// Each useInput hook adds a listener, so the count can legitimately exceed the default limit of 10.
|
|
38
|
+
internal_eventEmitter.current.setMaxListeners(Infinity);
|
|
39
|
+
// Store the currently attached readable listener to avoid stale closure issues
|
|
40
|
+
const readableListenerRef = useRef(undefined);
|
|
41
|
+
const inputParserRef = useRef(createInputParser());
|
|
42
|
+
const pendingInputFlushRef = useRef(undefined);
|
|
43
|
+
// Small delay to let chunked escape sequences complete before flushing as literal input.
|
|
44
|
+
const pendingInputFlushDelayMilliseconds = 20;
|
|
45
|
+
const clearPendingInputFlush = useCallback(() => {
|
|
46
|
+
if (!pendingInputFlushRef.current) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
clearTimeout(pendingInputFlushRef.current);
|
|
50
|
+
pendingInputFlushRef.current = undefined;
|
|
51
|
+
}, []);
|
|
52
|
+
const clearAnimationTimer = useCallback(() => {
|
|
53
|
+
if (!animationTimerRef.current) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
clearTimeout(animationTimerRef.current);
|
|
57
|
+
animationTimerRef.current = undefined;
|
|
58
|
+
}, []);
|
|
59
|
+
const scheduleAnimationTick = useCallback(() => {
|
|
60
|
+
clearAnimationTimer();
|
|
61
|
+
if (animationSubscribersRef.current.size === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let nextDueTime = Number.POSITIVE_INFINITY;
|
|
65
|
+
for (const subscriber of animationSubscribersRef.current.values()) {
|
|
66
|
+
// One shared timer is enough as long as it wakes at the earliest
|
|
67
|
+
// subscriber deadline and lets slower animations skip that tick.
|
|
68
|
+
nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
|
|
69
|
+
}
|
|
70
|
+
const delay = Math.max(0, nextDueTime - performance.now());
|
|
71
|
+
animationTimerRef.current = setTimeout(() => {
|
|
72
|
+
animationTimerRef.current = undefined;
|
|
73
|
+
const currentTime = performance.now();
|
|
74
|
+
for (const subscriber of animationSubscribersRef.current.values()) {
|
|
75
|
+
if (currentTime < subscriber.nextDueTime) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
subscriber.callback(currentTime);
|
|
79
|
+
const elapsedTime = currentTime - subscriber.startTime;
|
|
80
|
+
const elapsedFrames = Math.floor(elapsedTime / subscriber.interval) + 1;
|
|
81
|
+
// Advance from elapsed time rather than callback count so delayed
|
|
82
|
+
// ticks catch up instead of stretching the animation timeline.
|
|
83
|
+
subscriber.nextDueTime =
|
|
84
|
+
subscriber.startTime + elapsedFrames * subscriber.interval;
|
|
85
|
+
}
|
|
86
|
+
scheduleAnimationTick();
|
|
87
|
+
}, delay);
|
|
88
|
+
// Keep the timer ref'd while animations are active so `useAnimation()`
|
|
89
|
+
// can drive process lifetime in both interactive and non-interactive apps.
|
|
90
|
+
}, [clearAnimationTimer]);
|
|
91
|
+
const animationSubscribe = useCallback((callback, interval) => {
|
|
92
|
+
const startTime = performance.now();
|
|
93
|
+
// The scheduler owns the start timestamp so hooks can derive frames from
|
|
94
|
+
// the exact same origin that determines each subscriber's due time.
|
|
95
|
+
animationSubscribersRef.current.set(callback, {
|
|
96
|
+
callback,
|
|
97
|
+
interval,
|
|
98
|
+
startTime,
|
|
99
|
+
nextDueTime: startTime + interval,
|
|
100
|
+
});
|
|
101
|
+
scheduleAnimationTick();
|
|
102
|
+
return {
|
|
103
|
+
startTime,
|
|
104
|
+
unsubscribe() {
|
|
105
|
+
animationSubscribersRef.current.delete(callback);
|
|
106
|
+
if (animationSubscribersRef.current.size === 0) {
|
|
107
|
+
clearAnimationTimer();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
scheduleAnimationTick();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}, [clearAnimationTimer, scheduleAnimationTick]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
return () => {
|
|
116
|
+
clearAnimationTimer();
|
|
117
|
+
};
|
|
118
|
+
}, [clearAnimationTimer]);
|
|
33
119
|
// Determines if TTY is supported on the provided stdin
|
|
34
|
-
isRawModeSupported
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return (React.createElement(AppContext.Provider
|
|
39
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
40
|
-
, {
|
|
41
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
42
|
-
value: {
|
|
43
|
-
exit: this.handleExit,
|
|
44
|
-
} },
|
|
45
|
-
React.createElement(StdinContext.Provider
|
|
46
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
47
|
-
, {
|
|
48
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
49
|
-
value: {
|
|
50
|
-
stdin: this.props.stdin,
|
|
51
|
-
setRawMode: this.handleSetRawMode,
|
|
52
|
-
isRawModeSupported: this.isRawModeSupported(),
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
54
|
-
internal_exitOnCtrlC: this.props.exitOnCtrlC,
|
|
55
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
56
|
-
internal_eventEmitter: this.internal_eventEmitter,
|
|
57
|
-
} },
|
|
58
|
-
React.createElement(StdoutContext.Provider
|
|
59
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
60
|
-
, {
|
|
61
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
62
|
-
value: {
|
|
63
|
-
stdout: this.props.stdout,
|
|
64
|
-
write: this.props.writeToStdout,
|
|
65
|
-
} },
|
|
66
|
-
React.createElement(StderrContext.Provider
|
|
67
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
68
|
-
, {
|
|
69
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
70
|
-
value: {
|
|
71
|
-
stderr: this.props.stderr,
|
|
72
|
-
write: this.props.writeToStderr,
|
|
73
|
-
} },
|
|
74
|
-
React.createElement(FocusContext.Provider
|
|
75
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
76
|
-
, {
|
|
77
|
-
// eslint-disable-next-line react/jsx-no-constructed-context-values
|
|
78
|
-
value: {
|
|
79
|
-
activeId: this.state.activeFocusId,
|
|
80
|
-
add: this.addFocusable,
|
|
81
|
-
remove: this.removeFocusable,
|
|
82
|
-
activate: this.activateFocusable,
|
|
83
|
-
deactivate: this.deactivateFocusable,
|
|
84
|
-
enableFocus: this.enableFocus,
|
|
85
|
-
disableFocus: this.disableFocus,
|
|
86
|
-
focusNext: this.focusNext,
|
|
87
|
-
focusPrevious: this.focusPrevious,
|
|
88
|
-
focus: this.focus,
|
|
89
|
-
} }, this.state.error ? (React.createElement(ErrorOverview, { error: this.state.error })) : (this.props.children)))))));
|
|
90
|
-
}
|
|
91
|
-
componentDidMount() {
|
|
92
|
-
cliCursor.hide(this.props.stdout);
|
|
93
|
-
}
|
|
94
|
-
componentWillUnmount() {
|
|
95
|
-
cliCursor.show(this.props.stdout);
|
|
96
|
-
// ignore calling setRawMode on an handle stdin it cannot be called
|
|
97
|
-
if (this.isRawModeSupported()) {
|
|
98
|
-
this.handleSetRawMode(false);
|
|
120
|
+
const isRawModeSupported = stdin.isTTY;
|
|
121
|
+
const detachReadableListener = useCallback(() => {
|
|
122
|
+
if (!readableListenerRef.current) {
|
|
123
|
+
return;
|
|
99
124
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
stdin.removeListener('readable', readableListenerRef.current);
|
|
126
|
+
readableListenerRef.current = undefined;
|
|
127
|
+
}, [stdin]);
|
|
128
|
+
const disableRawMode = useCallback(() => {
|
|
129
|
+
stdin.setRawMode(false);
|
|
130
|
+
detachReadableListener();
|
|
131
|
+
stdin.unref();
|
|
132
|
+
rawModeEnabledCount.current = 0;
|
|
133
|
+
inputParserRef.current.reset();
|
|
134
|
+
clearPendingInputFlush();
|
|
135
|
+
}, [stdin, detachReadableListener, clearPendingInputFlush]);
|
|
136
|
+
const handleExit = useCallback((errorOrResult) => {
|
|
137
|
+
if (isRawModeSupported && rawModeEnabledCount.current > 0) {
|
|
138
|
+
disableRawMode();
|
|
139
|
+
}
|
|
140
|
+
onExit(errorOrResult);
|
|
141
|
+
}, [isRawModeSupported, disableRawMode, onExit]);
|
|
142
|
+
const handleInput = useCallback((input) => {
|
|
143
|
+
// Exit on Ctrl+C
|
|
144
|
+
// eslint-disable-next-line unicorn/no-hex-escape
|
|
145
|
+
if (input === '\x03' && exitOnCtrlC) {
|
|
146
|
+
handleExit();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Reset focus when there's an active focused component on Esc
|
|
150
|
+
if (input === escape) {
|
|
151
|
+
setActiveFocusId(undefined);
|
|
152
|
+
}
|
|
153
|
+
}, [exitOnCtrlC, handleExit]);
|
|
154
|
+
const emitInput = useCallback((input) => {
|
|
155
|
+
handleInput(input);
|
|
156
|
+
internal_eventEmitter.current.emit('input', input);
|
|
157
|
+
}, [handleInput]);
|
|
158
|
+
const schedulePendingInputFlush = useCallback(() => {
|
|
159
|
+
clearPendingInputFlush();
|
|
160
|
+
pendingInputFlushRef.current = setTimeout(() => {
|
|
161
|
+
pendingInputFlushRef.current = undefined;
|
|
162
|
+
const pendingEscape = inputParserRef.current.flushPendingEscape();
|
|
163
|
+
if (!pendingEscape) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
emitInput(pendingEscape);
|
|
167
|
+
}, pendingInputFlushDelayMilliseconds);
|
|
168
|
+
}, [clearPendingInputFlush, emitInput]);
|
|
169
|
+
const handleReadable = useCallback(() => {
|
|
170
|
+
clearPendingInputFlush();
|
|
171
|
+
let chunk;
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
173
|
+
while ((chunk = stdin.read()) !== null) {
|
|
174
|
+
const inputEvents = inputParserRef.current.push(chunk);
|
|
175
|
+
for (const event of inputEvents) {
|
|
176
|
+
if (typeof event === 'string') {
|
|
177
|
+
emitInput(event);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Keep paste on a separate channel from `useInput` so key handlers
|
|
181
|
+
// don't need to branch on mixed key-vs-paste event shapes.
|
|
182
|
+
if (internal_eventEmitter.current.listenerCount('paste') === 0) {
|
|
183
|
+
emitInput(event.paste);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
internal_eventEmitter.current.emit('paste', event.paste);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (inputParserRef.current.hasPendingEscape()) {
|
|
191
|
+
schedulePendingInputFlush();
|
|
192
|
+
}
|
|
193
|
+
}, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush]);
|
|
194
|
+
const handleSetRawMode = useCallback((isEnabled) => {
|
|
195
|
+
if (!isRawModeSupported) {
|
|
107
196
|
if (stdin === process.stdin) {
|
|
108
197
|
throw new Error('Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported');
|
|
109
198
|
}
|
|
@@ -114,173 +203,269 @@ export default class App extends PureComponent {
|
|
|
114
203
|
stdin.setEncoding('utf8');
|
|
115
204
|
if (isEnabled) {
|
|
116
205
|
// Ensure raw mode is enabled only once
|
|
117
|
-
if (
|
|
206
|
+
if (rawModeEnabledCount.current === 0) {
|
|
118
207
|
stdin.ref();
|
|
119
208
|
stdin.setRawMode(true);
|
|
120
|
-
|
|
209
|
+
// Store the listener reference to avoid stale closure when removing
|
|
210
|
+
readableListenerRef.current = handleReadable;
|
|
211
|
+
stdin.addListener('readable', handleReadable);
|
|
121
212
|
}
|
|
122
|
-
|
|
213
|
+
rawModeEnabledCount.current++;
|
|
123
214
|
return;
|
|
124
215
|
}
|
|
125
216
|
// Disable raw mode only when no components left that are using it
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
stdin.removeListener('readable', this.handleReadable);
|
|
129
|
-
stdin.unref();
|
|
217
|
+
if (rawModeEnabledCount.current === 0) {
|
|
218
|
+
return;
|
|
130
219
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let chunk;
|
|
134
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
135
|
-
while ((chunk = this.props.stdin.read()) !== null) {
|
|
136
|
-
this.handleInput(chunk);
|
|
137
|
-
this.internal_eventEmitter.emit('input', chunk);
|
|
220
|
+
if (--rawModeEnabledCount.current === 0) {
|
|
221
|
+
disableRawMode();
|
|
138
222
|
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (input === '\x03' && this.props.exitOnCtrlC) {
|
|
144
|
-
this.handleExit();
|
|
223
|
+
}, [isRawModeSupported, stdin, handleReadable, disableRawMode]);
|
|
224
|
+
const handleSetBracketedPasteMode = useCallback((isEnabled) => {
|
|
225
|
+
if (!stdout.isTTY) {
|
|
226
|
+
return;
|
|
145
227
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
228
|
+
if (isEnabled) {
|
|
229
|
+
if (bracketedPasteModeEnabledCount.current === 0) {
|
|
230
|
+
stdout.write('\u001B[?2004h');
|
|
231
|
+
}
|
|
232
|
+
bracketedPasteModeEnabledCount.current++;
|
|
233
|
+
return;
|
|
151
234
|
}
|
|
152
|
-
if (
|
|
235
|
+
if (bracketedPasteModeEnabledCount.current === 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (--bracketedPasteModeEnabledCount.current === 0) {
|
|
239
|
+
stdout.write('\u001B[?2004l');
|
|
240
|
+
}
|
|
241
|
+
}, [stdout]);
|
|
242
|
+
// Focus navigation helpers
|
|
243
|
+
const findNextFocusable = useCallback((currentFocusables, currentActiveFocusId) => {
|
|
244
|
+
const activeIndex = currentFocusables.findIndex(focusable => {
|
|
245
|
+
return focusable.id === currentActiveFocusId;
|
|
246
|
+
});
|
|
247
|
+
for (let index = activeIndex + 1; index < currentFocusables.length; index++) {
|
|
248
|
+
const focusable = currentFocusables[index];
|
|
249
|
+
if (focusable?.isActive) {
|
|
250
|
+
return focusable.id;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}, []);
|
|
255
|
+
const findPreviousFocusable = useCallback((currentFocusables, currentActiveFocusId) => {
|
|
256
|
+
const activeIndex = currentFocusables.findIndex(focusable => {
|
|
257
|
+
return focusable.id === currentActiveFocusId;
|
|
258
|
+
});
|
|
259
|
+
for (let index = activeIndex - 1; index >= 0; index--) {
|
|
260
|
+
const focusable = currentFocusables[index];
|
|
261
|
+
if (focusable?.isActive) {
|
|
262
|
+
return focusable.id;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}, []);
|
|
267
|
+
const focusNext = useCallback(() => {
|
|
268
|
+
setFocusables(currentFocusables => {
|
|
269
|
+
setActiveFocusId(currentActiveFocusId => {
|
|
270
|
+
const firstFocusableId = currentFocusables.find(focusable => focusable.isActive)?.id;
|
|
271
|
+
const nextFocusableId = findNextFocusable(currentFocusables, currentActiveFocusId);
|
|
272
|
+
return nextFocusableId ?? firstFocusableId;
|
|
273
|
+
});
|
|
274
|
+
return currentFocusables;
|
|
275
|
+
});
|
|
276
|
+
}, [findNextFocusable]);
|
|
277
|
+
const focusPrevious = useCallback(() => {
|
|
278
|
+
setFocusables(currentFocusables => {
|
|
279
|
+
setActiveFocusId(currentActiveFocusId => {
|
|
280
|
+
const lastFocusableId = currentFocusables.findLast(focusable => focusable.isActive)?.id;
|
|
281
|
+
const previousFocusableId = findPreviousFocusable(currentFocusables, currentActiveFocusId);
|
|
282
|
+
return previousFocusableId ?? lastFocusableId;
|
|
283
|
+
});
|
|
284
|
+
return currentFocusables;
|
|
285
|
+
});
|
|
286
|
+
}, [findPreviousFocusable]);
|
|
287
|
+
// Handle tab navigation via effect that subscribes to input events
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
const handleTabNavigation = (input) => {
|
|
290
|
+
if (!isFocusEnabled || focusablesCountRef.current === 0)
|
|
291
|
+
return;
|
|
153
292
|
if (input === tab) {
|
|
154
|
-
|
|
293
|
+
focusNext();
|
|
155
294
|
}
|
|
156
295
|
if (input === shiftTab) {
|
|
157
|
-
|
|
296
|
+
focusPrevious();
|
|
158
297
|
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
298
|
+
};
|
|
299
|
+
internal_eventEmitter.current.on('input', handleTabNavigation);
|
|
300
|
+
const emitter = internal_eventEmitter.current;
|
|
301
|
+
return () => {
|
|
302
|
+
emitter.off('input', handleTabNavigation);
|
|
303
|
+
};
|
|
304
|
+
}, [isFocusEnabled, focusNext, focusPrevious]);
|
|
305
|
+
const enableFocus = useCallback(() => {
|
|
306
|
+
setIsFocusEnabled(true);
|
|
307
|
+
}, []);
|
|
308
|
+
const disableFocus = useCallback(() => {
|
|
309
|
+
setIsFocusEnabled(false);
|
|
310
|
+
}, []);
|
|
311
|
+
const focus = useCallback((id) => {
|
|
312
|
+
setFocusables(currentFocusables => {
|
|
313
|
+
const hasFocusableId = currentFocusables.some(focusable => focusable?.id === id);
|
|
314
|
+
if (hasFocusableId) {
|
|
315
|
+
setActiveFocusId(id);
|
|
316
|
+
}
|
|
317
|
+
return currentFocusables;
|
|
170
318
|
});
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
319
|
+
}, []);
|
|
320
|
+
const addFocusable = useCallback((id, { autoFocus }) => {
|
|
321
|
+
setFocusables(currentFocusables => {
|
|
322
|
+
focusablesCountRef.current = currentFocusables.length + 1;
|
|
323
|
+
return [
|
|
324
|
+
...currentFocusables,
|
|
325
|
+
{
|
|
326
|
+
id,
|
|
327
|
+
isActive: true,
|
|
328
|
+
},
|
|
329
|
+
];
|
|
175
330
|
});
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return
|
|
331
|
+
if (autoFocus) {
|
|
332
|
+
setActiveFocusId(currentActiveFocusId => {
|
|
333
|
+
if (!currentActiveFocusId) {
|
|
334
|
+
return id;
|
|
335
|
+
}
|
|
336
|
+
return currentActiveFocusId;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}, []);
|
|
340
|
+
const removeFocusable = useCallback((id) => {
|
|
341
|
+
setActiveFocusId(currentActiveFocusId => {
|
|
342
|
+
if (currentActiveFocusId === id) {
|
|
343
|
+
return undefined;
|
|
182
344
|
}
|
|
183
|
-
return
|
|
345
|
+
return currentActiveFocusId;
|
|
184
346
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
activeFocusId: nextFocusableId ?? firstFocusableId,
|
|
192
|
-
};
|
|
347
|
+
setFocusables(currentFocusables => {
|
|
348
|
+
const filtered = currentFocusables.filter(focusable => {
|
|
349
|
+
return focusable.id !== id;
|
|
350
|
+
});
|
|
351
|
+
focusablesCountRef.current = filtered.length;
|
|
352
|
+
return filtered;
|
|
193
353
|
});
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
354
|
+
}, []);
|
|
355
|
+
const activateFocusable = useCallback((id) => {
|
|
356
|
+
setFocusables(currentFocusables => currentFocusables.map(focusable => {
|
|
357
|
+
if (focusable.id !== id) {
|
|
358
|
+
return focusable;
|
|
359
|
+
}
|
|
199
360
|
return {
|
|
200
|
-
|
|
361
|
+
id,
|
|
362
|
+
isActive: true,
|
|
201
363
|
};
|
|
364
|
+
}));
|
|
365
|
+
}, []);
|
|
366
|
+
const deactivateFocusable = useCallback((id) => {
|
|
367
|
+
setActiveFocusId(currentActiveFocusId => {
|
|
368
|
+
if (currentActiveFocusId === id) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
return currentActiveFocusId;
|
|
202
372
|
});
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
let nextFocusId = previousState.activeFocusId;
|
|
207
|
-
if (!nextFocusId && autoFocus) {
|
|
208
|
-
nextFocusId = id;
|
|
373
|
+
setFocusables(currentFocusables => currentFocusables.map(focusable => {
|
|
374
|
+
if (focusable.id !== id) {
|
|
375
|
+
return focusable;
|
|
209
376
|
}
|
|
210
377
|
return {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
...previousState.focusables,
|
|
214
|
-
{
|
|
215
|
-
id,
|
|
216
|
-
isActive: true,
|
|
217
|
-
},
|
|
218
|
-
],
|
|
378
|
+
id,
|
|
379
|
+
isActive: false,
|
|
219
380
|
};
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
removeFocusable = (id) => {
|
|
223
|
-
this.setState(previousState => ({
|
|
224
|
-
activeFocusId: previousState.activeFocusId === id
|
|
225
|
-
? undefined
|
|
226
|
-
: previousState.activeFocusId,
|
|
227
|
-
focusables: previousState.focusables.filter(focusable => {
|
|
228
|
-
return focusable.id !== id;
|
|
229
|
-
}),
|
|
230
|
-
}));
|
|
231
|
-
};
|
|
232
|
-
activateFocusable = (id) => {
|
|
233
|
-
this.setState(previousState => ({
|
|
234
|
-
focusables: previousState.focusables.map(focusable => {
|
|
235
|
-
if (focusable.id !== id) {
|
|
236
|
-
return focusable;
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
id,
|
|
240
|
-
isActive: true,
|
|
241
|
-
};
|
|
242
|
-
}),
|
|
243
|
-
}));
|
|
244
|
-
};
|
|
245
|
-
deactivateFocusable = (id) => {
|
|
246
|
-
this.setState(previousState => ({
|
|
247
|
-
activeFocusId: previousState.activeFocusId === id
|
|
248
|
-
? undefined
|
|
249
|
-
: previousState.activeFocusId,
|
|
250
|
-
focusables: previousState.focusables.map(focusable => {
|
|
251
|
-
if (focusable.id !== id) {
|
|
252
|
-
return focusable;
|
|
253
|
-
}
|
|
254
|
-
return {
|
|
255
|
-
id,
|
|
256
|
-
isActive: false,
|
|
257
|
-
};
|
|
258
|
-
}),
|
|
259
381
|
}));
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (focusable?.isActive) {
|
|
268
|
-
return focusable.id;
|
|
382
|
+
}, []);
|
|
383
|
+
// Handle cursor visibility, raw mode, and bracketed paste mode cleanup on unmount
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
return () => {
|
|
386
|
+
const canWriteToStdout = !stdout.destroyed && !stdout.writableEnded;
|
|
387
|
+
if (interactive && canWriteToStdout) {
|
|
388
|
+
cliCursor.show(stdout);
|
|
269
389
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
273
|
-
findPreviousFocusable = (state) => {
|
|
274
|
-
const activeIndex = state.focusables.findIndex(focusable => {
|
|
275
|
-
return focusable.id === state.activeFocusId;
|
|
276
|
-
});
|
|
277
|
-
for (let index = activeIndex - 1; index >= 0; index--) {
|
|
278
|
-
const focusable = state.focusables[index];
|
|
279
|
-
if (focusable?.isActive) {
|
|
280
|
-
return focusable.id;
|
|
390
|
+
if (isRawModeSupported && rawModeEnabledCount.current > 0) {
|
|
391
|
+
disableRawMode();
|
|
281
392
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
393
|
+
if (bracketedPasteModeEnabledCount.current > 0) {
|
|
394
|
+
if (stdout.isTTY && canWriteToStdout) {
|
|
395
|
+
stdout.write('\u001B[?2004l');
|
|
396
|
+
}
|
|
397
|
+
bracketedPasteModeEnabledCount.current = 0;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}, [stdout, isRawModeSupported, disableRawMode, interactive]);
|
|
401
|
+
// Memoize context values to prevent unnecessary re-renders
|
|
402
|
+
const appContextValue = useMemo(() => ({
|
|
403
|
+
exit: handleExit,
|
|
404
|
+
waitUntilRenderFlush: onWaitUntilRenderFlush,
|
|
405
|
+
}), [handleExit, onWaitUntilRenderFlush]);
|
|
406
|
+
const stdinContextValue = useMemo(() => ({
|
|
407
|
+
stdin,
|
|
408
|
+
setRawMode: handleSetRawMode,
|
|
409
|
+
setBracketedPasteMode: handleSetBracketedPasteMode,
|
|
410
|
+
isRawModeSupported,
|
|
411
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
412
|
+
internal_exitOnCtrlC: exitOnCtrlC,
|
|
413
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
414
|
+
internal_eventEmitter: internal_eventEmitter.current,
|
|
415
|
+
}), [
|
|
416
|
+
stdin,
|
|
417
|
+
handleSetRawMode,
|
|
418
|
+
handleSetBracketedPasteMode,
|
|
419
|
+
isRawModeSupported,
|
|
420
|
+
exitOnCtrlC,
|
|
421
|
+
]);
|
|
422
|
+
const stdoutContextValue = useMemo(() => ({
|
|
423
|
+
stdout,
|
|
424
|
+
write: writeToStdout,
|
|
425
|
+
}), [stdout, writeToStdout]);
|
|
426
|
+
const stderrContextValue = useMemo(() => ({
|
|
427
|
+
stderr,
|
|
428
|
+
write: writeToStderr,
|
|
429
|
+
}), [stderr, writeToStderr]);
|
|
430
|
+
const cursorContextValue = useMemo(() => ({
|
|
431
|
+
setCursorPosition,
|
|
432
|
+
}), [setCursorPosition]);
|
|
433
|
+
const focusContextValue = useMemo(() => ({
|
|
434
|
+
activeId: activeFocusId,
|
|
435
|
+
add: addFocusable,
|
|
436
|
+
remove: removeFocusable,
|
|
437
|
+
activate: activateFocusable,
|
|
438
|
+
deactivate: deactivateFocusable,
|
|
439
|
+
enableFocus,
|
|
440
|
+
disableFocus,
|
|
441
|
+
focusNext,
|
|
442
|
+
focusPrevious,
|
|
443
|
+
focus,
|
|
444
|
+
}), [
|
|
445
|
+
activeFocusId,
|
|
446
|
+
addFocusable,
|
|
447
|
+
removeFocusable,
|
|
448
|
+
activateFocusable,
|
|
449
|
+
deactivateFocusable,
|
|
450
|
+
enableFocus,
|
|
451
|
+
disableFocus,
|
|
452
|
+
focusNext,
|
|
453
|
+
focusPrevious,
|
|
454
|
+
focus,
|
|
455
|
+
]);
|
|
456
|
+
const animationContextValue = useMemo(() => ({
|
|
457
|
+
renderThrottleMs,
|
|
458
|
+
subscribe: animationSubscribe,
|
|
459
|
+
}), [animationSubscribe, renderThrottleMs]);
|
|
460
|
+
return (React.createElement(AppContext.Provider, { value: appContextValue },
|
|
461
|
+
React.createElement(StdinContext.Provider, { value: stdinContextValue },
|
|
462
|
+
React.createElement(StdoutContext.Provider, { value: stdoutContextValue },
|
|
463
|
+
React.createElement(StderrContext.Provider, { value: stderrContextValue },
|
|
464
|
+
React.createElement(FocusContext.Provider, { value: focusContextValue },
|
|
465
|
+
React.createElement(AnimationContext.Provider, { value: animationContextValue },
|
|
466
|
+
React.createElement(CursorContext.Provider, { value: cursorContextValue },
|
|
467
|
+
React.createElement(ErrorBoundary, { onError: handleExit }, children)))))))));
|
|
285
468
|
}
|
|
469
|
+
App.displayName = 'InternalApp';
|
|
470
|
+
export default App;
|
|
286
471
|
//# sourceMappingURL=App.js.map
|