-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
create-prompt.mts
113 lines (93 loc) · 3.4 KB
/
create-prompt.mts
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
112
113
import * as readline from 'node:readline';
import { CancelablePromise, type Prompt, type Prettify } from '@inquirer/type';
import MuteStream from 'mute-stream';
import { onExit as onSignalExit } from 'signal-exit';
import ScreenManager from './screen-manager.mjs';
import type { InquirerReadline } from './read-line.type.mjs';
import { withHooks, effectScheduler } from './hook-engine.mjs';
import { CancelPromptError, ExitPromptError } from './errors.mjs';
type ViewFunction<Value, Config> = (
config: Prettify<Config>,
done: (value: Value) => void,
) => string | [string, string | undefined];
export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
const prompt: Prompt<Value, Config> = (config, context) => {
// Default `input` to stdin
const input = context?.input ?? process.stdin;
// Add mute capabilities to the output
const output = new MuteStream();
output.pipe(context?.output ?? process.stdout);
const rl = readline.createInterface({
terminal: true,
input,
output,
}) as InquirerReadline;
const screen = new ScreenManager(rl);
let cancel: () => void = () => {};
const answer = new CancelablePromise<Value>((resolve, reject) => {
withHooks(rl, (store) => {
function checkCursorPos() {
screen.checkCursorPos();
}
const removeExitListener = onSignalExit((code, signal) => {
onExit();
reject(
new ExitPromptError(`User force closed the prompt with ${code} ${signal}`),
);
});
function onExit() {
try {
store.hooksCleanup.forEach((cleanFn) => {
cleanFn?.();
});
} catch (error) {
reject(error);
}
if (context?.clearPromptOnDone) {
screen.clean();
} else {
screen.clearContent();
}
screen.done();
removeExitListener();
store.rl.input.removeListener('keypress', checkCursorPos);
}
cancel = () => {
onExit();
reject(new CancelPromptError());
};
function done(value: Value) {
// Delay execution to let time to the hookCleanup functions to registers.
setImmediate(() => {
onExit();
// Finally we resolve our promise
resolve(value);
});
}
function workLoop(resolvedConfig: Config) {
store.index = 0;
store.handleChange = () => workLoop(resolvedConfig);
try {
const nextView = view(config, done);
const [content, bottomContent] =
typeof nextView === 'string' ? [nextView] : nextView;
screen.render(content, bottomContent);
effectScheduler.run();
} catch (error) {
onExit();
reject(error);
}
}
workLoop(config);
// Re-renders only happen when the state change; but the readline cursor could change position
// and that also requires a re-render (and a manual one because we mute the streams).
// We set the listener after the initial workLoop to avoid a double render if render triggered
// by a state change sets the cursor to the right position.
store.rl.input.on('keypress', checkCursorPos);
});
});
answer.cancel = cancel;
return answer;
};
return prompt;
}