1
1
import * as readline from 'node:readline' ;
2
2
import { AsyncResource } from 'node:async_hooks' ;
3
- import { CancelablePromise , type Prompt , type Prettify } from '@inquirer/type' ;
3
+ import { type Prompt , type Prettify } from '@inquirer/type' ;
4
4
import MuteStream from 'mute-stream' ;
5
5
import { onExit as onSignalExit } from 'signal-exit' ;
6
6
import ScreenManager from './screen-manager.mjs' ;
7
- import type { InquirerReadline } from '@inquirer/type' ;
7
+ import { CancelablePromise , type InquirerReadline } from '@inquirer/type' ;
8
8
import { withHooks , effectScheduler } from './hook-engine.mjs' ;
9
9
import { CancelPromptError , ExitPromptError } from './errors.mjs' ;
10
10
@@ -14,13 +14,13 @@ type ViewFunction<Value, Config> = (
14
14
) => string | [ string , string | undefined ] ;
15
15
16
16
export function createPrompt < Value , Config > ( view : ViewFunction < Value , Config > ) {
17
- const prompt : Prompt < Value , Config > = ( config , context ) => {
17
+ const prompt : Prompt < Value , Config > = ( config , context = { } ) => {
18
18
// Default `input` to stdin
19
- const input = context ?. input ?? process . stdin ;
19
+ const { input = process . stdin } = context ;
20
20
21
21
// Add mute capabilities to the output
22
22
const output = new MuteStream ( ) ;
23
- output . pipe ( context ? .output ?? process . stdout ) ;
23
+ output . pipe ( context . output ?? process . stdout ) ;
24
24
25
25
const rl = readline . createInterface ( {
26
26
terminal : true ,
@@ -29,84 +29,82 @@ export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
29
29
} ) as InquirerReadline ;
30
30
const screen = new ScreenManager ( rl ) ;
31
31
32
- let cancel : ( ) => void = ( ) => { } ;
33
- const answer = new CancelablePromise < Value > ( ( resolve , reject ) => {
34
- withHooks ( rl , ( cycle ) => {
35
- function checkCursorPos ( ) {
36
- screen . checkCursorPos ( ) ;
37
- }
32
+ const cleanups = new Set < ( ) => void > ( ) ;
33
+ const { promise, resolve, reject } = CancelablePromise . withResolver < Value > ( ) ;
38
34
39
- const removeExitListener = onSignalExit ( ( code , signal ) => {
40
- onExit ( ) ;
41
- reject (
42
- new ExitPromptError ( `User force closed the prompt with ${ code } ${ signal } ` ) ,
43
- ) ;
44
- } ) ;
35
+ function onExit ( ) {
36
+ cleanups . forEach ( ( cleanup ) => cleanup ( ) ) ;
45
37
46
- const hooksCleanup = AsyncResource . bind ( ( ) => {
47
- try {
48
- effectScheduler . clearAll ( ) ;
49
- } catch ( error ) {
50
- reject ( error ) ;
51
- }
52
- } ) ;
53
-
54
- function onExit ( ) {
55
- hooksCleanup ( ) ;
38
+ screen . done ( { clearContent : Boolean ( context ?. clearPromptOnDone ) } ) ;
39
+ output . end ( ) ;
40
+ }
56
41
57
- screen . done ( { clearContent : Boolean ( context ?. clearPromptOnDone ) } ) ;
42
+ function fail ( error : unknown ) {
43
+ onExit ( ) ;
44
+ reject ( error ) ;
45
+ }
58
46
59
- removeExitListener ( ) ;
60
- rl . input . removeListener ( 'keypress' , checkCursorPos ) ;
61
- rl . removeListener ( 'close' , hooksCleanup ) ;
62
- output . end ( ) ;
47
+ withHooks ( rl , ( cycle ) => {
48
+ cleanups . add (
49
+ onSignalExit ( ( code , signal ) => {
50
+ fail (
51
+ new ExitPromptError ( `User force closed the prompt with ${ code } ${ signal } ` ) ,
52
+ ) ;
53
+ } ) ,
54
+ ) ;
55
+
56
+ const hooksCleanup = AsyncResource . bind ( ( ) => {
57
+ try {
58
+ effectScheduler . clearAll ( ) ;
59
+ } catch ( error ) {
60
+ reject ( error ) ;
63
61
}
64
-
65
- cancel = ( ) => {
62
+ } ) ;
63
+ cleanups . add ( hooksCleanup ) ;
64
+
65
+ // Re-renders only happen when the state change; but the readline cursor could change position
66
+ // and that also requires a re-render (and a manual one because we mute the streams).
67
+ // We set the listener after the initial workLoop to avoid a double render if render triggered
68
+ // by a state change sets the cursor to the right position.
69
+ const checkCursorPos = ( ) => screen . checkCursorPos ( ) ;
70
+ rl . input . on ( 'keypress' , checkCursorPos ) ;
71
+ cleanups . add ( ( ) => rl . input . removeListener ( 'keypress' , checkCursorPos ) ) ;
72
+
73
+ // The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
74
+ // triggers after the process is done (which happens after timeouts are done triggering.)
75
+ // We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
76
+ rl . on ( 'close' , hooksCleanup ) ;
77
+ cleanups . add ( ( ) => rl . removeListener ( 'close' , hooksCleanup ) ) ;
78
+
79
+ function done ( value : Value ) {
80
+ // Delay execution to let time to the hookCleanup functions to registers.
81
+ setImmediate ( ( ) => {
66
82
onExit ( ) ;
67
- reject ( new CancelPromptError ( ) ) ;
68
- } ;
69
-
70
- function done ( value : Value ) {
71
- // Delay execution to let time to the hookCleanup functions to registers.
72
- setImmediate ( ( ) => {
73
- onExit ( ) ;
74
-
75
- // Finally we resolve our promise
76
- resolve ( value ) ;
77
- } ) ;
78
- }
79
83
80
- cycle ( ( ) => {
81
- try {
82
- const nextView = view ( config , done ) ;
83
-
84
- const [ content , bottomContent ] =
85
- typeof nextView === 'string' ? [ nextView ] : nextView ;
86
- screen . render ( content , bottomContent ) ;
87
-
88
- effectScheduler . run ( ) ;
89
- } catch ( error ) {
90
- onExit ( ) ;
91
- reject ( error ) ;
92
- }
84
+ // Finally we resolve our promise
85
+ resolve ( value ) ;
93
86
} ) ;
87
+ }
94
88
95
- // Re-renders only happen when the state change; but the readline cursor could change position
96
- // and that also requires a re-render (and a manual one because we mute the streams).
97
- // We set the listener after the initial workLoop to avoid a double render if render triggered
98
- // by a state change sets the cursor to the right position.
99
- rl . input . on ( 'keypress' , checkCursorPos ) ;
89
+ cycle ( ( ) => {
90
+ try {
91
+ const nextView = view ( config , done ) ;
100
92
101
- // The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
102
- // triggers after the process is done (which happens after timeouts are done triggering.)
103
- // We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
104
- rl . on ( 'close' , hooksCleanup ) ;
93
+ const [ content , bottomContent ] =
94
+ typeof nextView === 'string' ? [ nextView ] : nextView ;
95
+ screen . render ( content , bottomContent ) ;
96
+
97
+ effectScheduler . run ( ) ;
98
+ } catch ( error : unknown ) {
99
+ fail ( error ) ;
100
+ }
105
101
} ) ;
106
102
} ) ;
107
103
108
- answer . cancel = cancel ;
109
- return answer ;
104
+ promise . cancel = ( ) => {
105
+ fail ( new CancelPromptError ( ) ) ;
106
+ } ;
107
+ return promise ;
110
108
} ;
111
109
112
110
return prompt ;
0 commit comments