-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Expand file tree
/
Copy pathuse-input.ts
More file actions
112 lines (99 loc) · 4.08 KB
/
use-input.ts
File metadata and controls
112 lines (99 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { useLayoutEffect } from 'react'
import { useEventCallback } from 'usehooks-ts'
import type { InputEvent, Key } from '../events/input-event.js'
import useStdin from './use-stdin.js'
type Handler = (input: string, key: Key, event: InputEvent) => void
type Options = {
/**
* 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.
*
* @default true
*/
isActive?: boolean
}
/**
* This hook is used for handling user input.
* It's a more convenient alternative to using `StdinContext` and listening to `data` events.
* The callback you pass to `useInput` is called for each character when user enters any input.
* However, if 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`.
*
* ```
* import {useInput} from 'ink';
*
* const UserInput = () => {
* useInput((input, key) => {
* if (input === 'q') {
* // Exit program
* }
*
* if (key.leftArrow) {
* // Left arrow key pressed
* }
* });
*
* return …
* };
* ```
*/
const useInput = (inputHandler: Handler, options: Options = {}) => {
const { setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdin()
// useLayoutEffect (not useEffect) so that raw mode is enabled synchronously
// during React's commit phase, before render() returns. With useEffect, raw
// mode setup is deferred to the next event loop tick via React's scheduler,
// leaving the terminal in cooked mode — keystrokes echo and the cursor is
// visible until the effect fires.
useLayoutEffect(() => {
if (options.isActive === false) {
return
}
setRawMode(true)
return () => {
// Re-check isActive so we skip the (false → false) path
// produced by Ink's PreserveFocus unmount/remount cycles
// during MCP async re-render churn. A single commit round-trip
// unmounts the leaf and re-mounts it before the next tick;
// keeping raw mode on avoids deregistering the stdin listener.
if (options.isActive !== false) {
return
}
setRawMode(false)
}
}, [options.isActive, setRawMode])
// Register the listener once on mount so its slot in the EventEmitter's
// listener array is stable. If isActive were in the effect's deps, the
// listener would re-append on false→true, moving it behind listeners
// that registered while it was inactive — breaking
// stopImmediatePropagation() ordering. useEventCallback keeps the
// reference stable while reading latest isActive/inputHandler from
// closure (it syncs via useLayoutEffect, so it's compiler-safe).
//
// Use useLayoutEffect (not useEffect) so the handler is registered
// synchronously during the commit phase, before any stdin data can be
// processed. In data mode, stdin.write() fires handleDataChunk
// synchronously, which calls processInput → discreteUpdates → emit('input').
// If the handler were in useEffect (passive effect, fires asynchronously
// after the scheduler flushes), there's a window where stdin has a
// listener but the EventEmitter has no handlers — keys are silently
// dropped. This is safe because EventEmitter listener registration is
// synchronous, lightweight, and has no visual side effects.
const handleData = useEventCallback((event: InputEvent) => {
if (options.isActive === false) {
return
}
const { input, key } = event
// If app is not supposed to exit on Ctrl+C, then let input listener handle it
// Note: discreteUpdates is called at the App level when emitting events,
// so all listeners are already within a high-priority update context.
if (!(input === 'c' && key.ctrl) || !internal_exitOnCtrlC) {
inputHandler(input, key, event)
}
})
useLayoutEffect(() => {
internal_eventEmitter?.on('input', handleData)
return () => {
internal_eventEmitter?.removeListener('input', handleData)
}
}, [internal_eventEmitter, handleData])
}
export default useInput