caique 0.2.0 → 0.3.1
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/README.md +3 -5
- package/dist/ask.js +0 -46
- package/dist/binding.js +2 -32
- package/dist/clack.d.ts +44 -0
- package/dist/clack.js +97 -0
- package/dist/decide.js +0 -33
- package/dist/index.js +0 -12
- package/dist/inquirer-errors.d.ts +38 -0
- package/dist/inquirer-errors.js +21 -0
- package/dist/inquirer-hooks.d.ts +86 -0
- package/dist/inquirer-hooks.js +124 -0
- package/dist/inquirer-keys.d.ts +31 -0
- package/dist/inquirer-keys.js +22 -0
- package/dist/inquirer-screen.d.ts +131 -0
- package/dist/inquirer-screen.js +122 -0
- package/dist/inquirer-theme.d.ts +58 -0
- package/dist/inquirer-theme.js +51 -0
- package/dist/inquirer.d.ts +72 -0
- package/dist/inquirer.js +207 -0
- package/dist/plugin.js +0 -64
- package/dist/raw.js +0 -60
- package/dist/runtime.js +0 -12
- package/dist/schema.json +1 -300
- package/dist/spec.js +0 -26
- package/dist/terminal.js +0 -36
- package/package.json +15 -4
package/dist/inquirer.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import exitHook from 'closeout/exit-hook';
|
|
5
|
+
import { AbortPromptError, CancelPromptError, ExitPromptError } from './inquirer-errors.js';
|
|
6
|
+
import { effectScheduler, useEffect, useRef, useState, withHooks, withUpdates } from './inquirer-hooks.js';
|
|
7
|
+
import { MuteStream, ScreenManager } from './inquirer-screen.js';
|
|
8
|
+
import { LINE, makeTheme } from './inquirer-theme.js';
|
|
9
|
+
import { processRuntime } from './runtime.js';
|
|
10
|
+
const SEPARATOR_WIDTH = 15;
|
|
11
|
+
const SPINNER_DELAY_MS = 300;
|
|
12
|
+
export class Separator {
|
|
13
|
+
separator = Array.from({ length: SEPARATOR_WIDTH }).join(LINE);
|
|
14
|
+
type = 'separator';
|
|
15
|
+
constructor(separator) {
|
|
16
|
+
if (separator !== undefined && separator !== '')
|
|
17
|
+
this.separator = separator;
|
|
18
|
+
}
|
|
19
|
+
static isSeparator(choice) {
|
|
20
|
+
return Boolean(choice) && typeof choice === 'object' && choice !== null && 'type' in choice && choice.type === 'separator';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function usePrefix({ status = 'idle', theme }) {
|
|
24
|
+
const [showLoader, setShowLoader] = useState(false);
|
|
25
|
+
const [tick, setTick] = useState(0);
|
|
26
|
+
const { prefix, spinner } = makeTheme(theme);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (status !== 'loading') {
|
|
29
|
+
setShowLoader(false);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
let tickInterval;
|
|
33
|
+
let inc = -1;
|
|
34
|
+
const delayTimeout = setTimeout(() => {
|
|
35
|
+
setShowLoader(true);
|
|
36
|
+
tickInterval = setInterval(() => {
|
|
37
|
+
inc = inc + 1;
|
|
38
|
+
setTick(inc % spinner.frames.length);
|
|
39
|
+
}, spinner.interval);
|
|
40
|
+
}, SPINNER_DELAY_MS);
|
|
41
|
+
return () => {
|
|
42
|
+
clearTimeout(delayTimeout);
|
|
43
|
+
clearInterval(tickInterval);
|
|
44
|
+
};
|
|
45
|
+
}, [status]);
|
|
46
|
+
if (showLoader === true)
|
|
47
|
+
return spinner.frames[tick ?? 0] ?? '';
|
|
48
|
+
const iconName = status === 'loading' ? 'idle' : status;
|
|
49
|
+
if (typeof prefix === 'string')
|
|
50
|
+
return prefix;
|
|
51
|
+
return prefix[iconName] ?? prefix['idle'] ?? '';
|
|
52
|
+
}
|
|
53
|
+
export function useKeypress(userHandler) {
|
|
54
|
+
const signal = useRef(userHandler);
|
|
55
|
+
signal.current = userHandler;
|
|
56
|
+
useEffect((rl) => {
|
|
57
|
+
let ignore = false;
|
|
58
|
+
const handler = withUpdates((_input, event) => {
|
|
59
|
+
if (ignore)
|
|
60
|
+
return;
|
|
61
|
+
void signal.current(event, rl);
|
|
62
|
+
});
|
|
63
|
+
rl.input.on('keypress', handler);
|
|
64
|
+
return () => {
|
|
65
|
+
ignore = true;
|
|
66
|
+
rl.input.removeListener('keypress', handler);
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
}
|
|
70
|
+
function callerFile() {
|
|
71
|
+
const saved = Error.prepareStackTrace;
|
|
72
|
+
let frames = [];
|
|
73
|
+
try {
|
|
74
|
+
Error.prepareStackTrace = (_error, callSites) => {
|
|
75
|
+
frames = [...callSites];
|
|
76
|
+
return frames;
|
|
77
|
+
};
|
|
78
|
+
void new Error('trace').stack;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
Error.prepareStackTrace = saved;
|
|
84
|
+
const fileName = frames[2]?.getFileName() ?? undefined;
|
|
85
|
+
if (fileName === undefined || fileName.startsWith('file://'))
|
|
86
|
+
return fileName;
|
|
87
|
+
return resolvePath(fileName);
|
|
88
|
+
}
|
|
89
|
+
function listenTo(target, event, listener) {
|
|
90
|
+
const [add, remove] = 'on' in target
|
|
91
|
+
? [target.on.bind(target), target.removeListener.bind(target)]
|
|
92
|
+
: [target.addEventListener.bind(target), target.removeEventListener.bind(target)];
|
|
93
|
+
add(event, listener);
|
|
94
|
+
return () => {
|
|
95
|
+
remove(event, listener);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const nativeSetImmediate = globalThis.setImmediate;
|
|
99
|
+
function readlineOver(input, output) {
|
|
100
|
+
const stream = output;
|
|
101
|
+
return createInterface({ terminal: true, input, output: stream });
|
|
102
|
+
}
|
|
103
|
+
function runPrompt(view, origin, config, context) {
|
|
104
|
+
const runtime = processRuntime();
|
|
105
|
+
const { input = runtime.stdin, signal } = context;
|
|
106
|
+
const cleanups = new Set();
|
|
107
|
+
const output = new MuteStream();
|
|
108
|
+
output.pipe((context.output ?? runtime.stdout));
|
|
109
|
+
const rl = readlineOver(input, output);
|
|
110
|
+
output.mute();
|
|
111
|
+
const screen = new ScreenManager(rl);
|
|
112
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
113
|
+
return withHooks(rl, (cycle) => {
|
|
114
|
+
const clearEffects = AsyncResource.bind(() => {
|
|
115
|
+
effectScheduler.clearAll();
|
|
116
|
+
});
|
|
117
|
+
const settlePrompt = (settle) => {
|
|
118
|
+
try {
|
|
119
|
+
clearEffects();
|
|
120
|
+
settle();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const settler = (finish) => (settled) => {
|
|
127
|
+
settlePrompt(() => {
|
|
128
|
+
finish(settled);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
const resolvePrompt = settler(resolve);
|
|
132
|
+
const rejectPrompt = settler(reject);
|
|
133
|
+
const endsWith = (make) => () => {
|
|
134
|
+
rejectPrompt(make());
|
|
135
|
+
};
|
|
136
|
+
const promptPromise = Object.assign(promise
|
|
137
|
+
.finally(() => {
|
|
138
|
+
for (const cleanup of cleanups)
|
|
139
|
+
cleanup();
|
|
140
|
+
screen.done({ clearContent: Boolean(context.clearPromptOnDone) });
|
|
141
|
+
output.end();
|
|
142
|
+
})
|
|
143
|
+
.then(() => promise), { cancel: endsWith(() => new CancelPromptError()) });
|
|
144
|
+
if (signal) {
|
|
145
|
+
const abort = endsWith(() => new AbortPromptError({ cause: signal.reason }));
|
|
146
|
+
if (signal.aborted) {
|
|
147
|
+
abort();
|
|
148
|
+
return promptPromise;
|
|
149
|
+
}
|
|
150
|
+
cleanups.add(listenTo(signal, 'abort', abort));
|
|
151
|
+
}
|
|
152
|
+
const forceClosed = (how) => {
|
|
153
|
+
rejectPrompt(new ExitPromptError(`User force closed the prompt with ${how}`));
|
|
154
|
+
};
|
|
155
|
+
cleanups.add(exitHook((code) => {
|
|
156
|
+
forceClosed(String(code));
|
|
157
|
+
}));
|
|
158
|
+
cleanups.add(listenTo(rl, 'SIGINT', () => {
|
|
159
|
+
forceClosed('SIGINT');
|
|
160
|
+
}));
|
|
161
|
+
cleanups.add(listenTo(rl, 'close', clearEffects));
|
|
162
|
+
const startCycle = () => {
|
|
163
|
+
cleanups.add(listenTo(rl.input, 'keypress', () => {
|
|
164
|
+
screen.checkCursorPos();
|
|
165
|
+
}));
|
|
166
|
+
let pendingDone = null;
|
|
167
|
+
cycle(() => {
|
|
168
|
+
let effectsSettled = false;
|
|
169
|
+
try {
|
|
170
|
+
const nextView = view(config, (value) => {
|
|
171
|
+
if (effectsSettled)
|
|
172
|
+
resolvePrompt(value);
|
|
173
|
+
else
|
|
174
|
+
pendingDone = { value };
|
|
175
|
+
});
|
|
176
|
+
if (nextView === undefined)
|
|
177
|
+
throw new Error(`Prompt functions must return a string.\n at ${origin ?? '<unknown>'}`);
|
|
178
|
+
const [content, bottomContent] = typeof nextView === 'string' ? [nextView, undefined] : nextView;
|
|
179
|
+
screen.render(content, bottomContent);
|
|
180
|
+
effectScheduler.run();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
rejectPrompt(error);
|
|
184
|
+
}
|
|
185
|
+
effectsSettled = true;
|
|
186
|
+
if (pendingDone !== null) {
|
|
187
|
+
const { value } = pendingDone;
|
|
188
|
+
pendingDone = null;
|
|
189
|
+
resolvePrompt(value);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
if ('readableFlowing' in input)
|
|
194
|
+
nativeSetImmediate(startCycle);
|
|
195
|
+
else
|
|
196
|
+
startCycle();
|
|
197
|
+
return promptPromise;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export function createPrompt(view) {
|
|
201
|
+
const origin = callerFile();
|
|
202
|
+
return (config, context = {}) => runPrompt(view, origin, config, context);
|
|
203
|
+
}
|
|
204
|
+
export { AbortPromptError, CancelPromptError, ExitPromptError, HookError, ValidationError } from './inquirer-errors.js';
|
|
205
|
+
export { useEffect, useMemo, useRef, useState } from './inquirer-hooks.js';
|
|
206
|
+
export { getDefaultKeybindings, isBackspaceKey, isDownKey, isEnterKey, isNumberKey, isShiftKey, isSpaceKey, isTabKey, isUpKey, } from './inquirer-keys.js';
|
|
207
|
+
export { defaultTheme, getDefaultTheme, makeTheme } from './inquirer-theme.js';
|
package/dist/plugin.js
CHANGED
|
@@ -1,37 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The plugin host for caique's half of the contract (`plugin-contract` R1, R5, R6, R7, R8).
|
|
3
|
-
*
|
|
4
|
-
* A plugin is one plain object shared by the whole family. This file keeps the key caique
|
|
5
|
-
* understands — `widgets`, a prompt kind and how to draw it — and **ignores every other key
|
|
6
|
-
* without complaining**, which is what makes the same object work on any subset of the
|
|
7
|
-
* family that is installed. A plugin written for flagstaff registers here and contributes
|
|
8
|
-
* its widgets; its `tokens`, `glyphs` and `components` are not caique's business and are
|
|
9
|
-
* not an error.
|
|
10
|
-
*
|
|
11
|
-
* **Nothing here imports another layer**, and the plugin shape is declared rather than
|
|
12
|
-
* imported (R3). Types erase, so an import would cost nothing at run time — and it would
|
|
13
|
-
* still put flagstaff in caique's dependency story, which is the one thing the family
|
|
14
|
-
* promises it does not do.
|
|
15
|
-
*
|
|
16
|
-
* **A widget is the same shape a flagstaff component is** — `{ static, frame?, sample? }` —
|
|
17
|
-
* and a widget without `static` is refused with `E_NO_STATIC_PROJECTION`, the same code and
|
|
18
|
-
* the same fix shape. That sameness is the whole content of R5: the moment the two shapes
|
|
19
|
-
* differ by a key, "the same shape" stops being a fact a lock can hold and becomes prose.
|
|
20
|
-
*
|
|
21
|
-
* **Why this file, and not `ask.ts`, raises `E_UNKNOWN_KIND`.** `PromptKind` is an open
|
|
22
|
-
* union now, so `{ kind: 'acme-rating' }` type-checks whether or not anyone can draw it.
|
|
23
|
-
* The refusal therefore has to happen where the registry is, and it has to be *loud*: a
|
|
24
|
-
* kind nobody registered rendered as a text prompt is the silent-wrong-answer failure this
|
|
25
|
-
* package exists to prevent, wearing a hat.
|
|
26
|
-
*/
|
|
27
1
|
import { projection } from './ask.js';
|
|
28
2
|
import { BUILT_IN_KINDS } from './spec.js';
|
|
29
|
-
/**
|
|
30
|
-
* The plugin contract version. One number for the family — the same `1` flagstaff and
|
|
31
|
-
* roundel declare, written out rather than imported for the reason in the file comment.
|
|
32
|
-
*/
|
|
33
3
|
export const CONTRACT = 1;
|
|
34
|
-
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
35
4
|
export class PluginError extends Error {
|
|
36
5
|
code;
|
|
37
6
|
fix;
|
|
@@ -43,14 +12,7 @@ export class PluginError extends Error {
|
|
|
43
12
|
}
|
|
44
13
|
}
|
|
45
14
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
46
|
-
/** The six, in the order a person reads them — for the `fix` on an unknown kind. */
|
|
47
15
|
const builtIns = () => [...BUILT_IN_KINDS].join(', ');
|
|
48
|
-
/**
|
|
49
|
-
* Refuse a plugin that cannot contribute a widget, at the door.
|
|
50
|
-
*
|
|
51
|
-
* Every refusal here is about the `widgets` key or the plugin's own identity. A key another
|
|
52
|
-
* layer owns is not inspected and not rejected (R1) — caique has no opinion about a spinner.
|
|
53
|
-
*/
|
|
54
16
|
export function validate(plugin) {
|
|
55
17
|
if (!isRecord(plugin))
|
|
56
18
|
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin is a plain object', 'export an object, not a function or an array');
|
|
@@ -71,19 +33,11 @@ function validateWidgets(widgetMap, name) {
|
|
|
71
33
|
for (const [kind, widget] of Object.entries(widgetMap)) {
|
|
72
34
|
if (kind === '')
|
|
73
35
|
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": a widget’s kind is empty`, 'name the kind — it is what a `PromptSpec` sets as `kind`');
|
|
74
|
-
/**
|
|
75
|
-
* A plugin may not replace one of the six. The built-ins are the accessible floor and
|
|
76
|
-
* the drop-in surface, and `password` in particular guarantees that nothing writes back
|
|
77
|
-
* what it read — a third party that could override it could defeat that from a config
|
|
78
|
-
* file. Extension is the space *outside* the six, which is exactly what widening
|
|
79
|
-
* `PromptKind` opened up.
|
|
80
|
-
*/
|
|
81
36
|
if (BUILT_IN_KINDS.has(kind)) {
|
|
82
37
|
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": "${kind}" is a built-in prompt kind`, `caique draws the six itself (${builtIns()}); name a kind of your own`);
|
|
83
38
|
}
|
|
84
39
|
if (!isRecord(widget))
|
|
85
40
|
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": widget "${kind}" is not an object`, 'a widget is `{ static, frame?, sample? }`');
|
|
86
|
-
/** U3 everywhere else, and the reason a third-party prompt cannot break the non-TTY guarantee. */
|
|
87
41
|
if (typeof widget['static'] !== 'function') {
|
|
88
42
|
throw new PluginError('E_NO_STATIC_PROJECTION', `plugin "${name}": widget "${kind}" has no static projection`, 'add `static: (spec) => "…"` — it is what a pipe, an agent and a screen reader get');
|
|
89
43
|
}
|
|
@@ -101,23 +55,16 @@ function validateSample(sample, name, kind) {
|
|
|
101
55
|
}
|
|
102
56
|
}
|
|
103
57
|
const order = [];
|
|
104
|
-
/**
|
|
105
|
-
* Register a plugin. Later wins, like ESLint flat config: the array is ordered, a caller
|
|
106
|
-
* reads it top to bottom, and the last word on a kind is the one nearest the program.
|
|
107
|
-
*/
|
|
108
58
|
export function register(plugin) {
|
|
109
59
|
validate(plugin);
|
|
110
60
|
order.push(plugin);
|
|
111
61
|
}
|
|
112
|
-
/** Forget every registered plugin. For tests, and for a program that re-registers at runtime. */
|
|
113
62
|
export function reset() {
|
|
114
63
|
order.length = 0;
|
|
115
64
|
}
|
|
116
|
-
/** The plugins registered, in registration order. */
|
|
117
65
|
export function registered() {
|
|
118
66
|
return order;
|
|
119
67
|
}
|
|
120
|
-
/** Every kind a plugin contributed, with who won it and who it shadowed. */
|
|
121
68
|
export function widgets() {
|
|
122
69
|
const by = new Map();
|
|
123
70
|
for (const plugin of order) {
|
|
@@ -128,9 +75,7 @@ export function widgets() {
|
|
|
128
75
|
}
|
|
129
76
|
return [...by.values()];
|
|
130
77
|
}
|
|
131
|
-
/** The widget that draws `kind`, or nothing when no plugin registered one. */
|
|
132
78
|
export function widgetFor(kind) {
|
|
133
|
-
// Walked backwards because later wins, and the first hit from the end is the winner.
|
|
134
79
|
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
135
80
|
const widget = order[i].widgets?.[kind];
|
|
136
81
|
if (widget !== undefined)
|
|
@@ -138,17 +83,9 @@ export function widgetFor(kind) {
|
|
|
138
83
|
}
|
|
139
84
|
return undefined;
|
|
140
85
|
}
|
|
141
|
-
/** Every kind that can be drawn right now: the six, plus whatever is registered. */
|
|
142
86
|
export function kinds() {
|
|
143
87
|
return [...BUILT_IN_KINDS, ...widgets().map((c) => c.kind)];
|
|
144
88
|
}
|
|
145
|
-
/**
|
|
146
|
-
* The static projection for *any* kind — the one surface a caller needs.
|
|
147
|
-
*
|
|
148
|
-
* The six are still drawn by caique; anything else is a registered widget's `static`. A
|
|
149
|
-
* kind that is neither is a refusal naming what *is* registered, so the reader can see the
|
|
150
|
-
* typo rather than a text prompt where their rating widget should have been.
|
|
151
|
-
*/
|
|
152
89
|
export function projectionOf(spec) {
|
|
153
90
|
if (BUILT_IN_KINDS.has(spec.kind))
|
|
154
91
|
return projection(spec);
|
|
@@ -160,4 +97,3 @@ export function projectionOf(spec) {
|
|
|
160
97
|
}
|
|
161
98
|
return widget.static(spec).trimEnd();
|
|
162
99
|
}
|
|
163
|
-
//# sourceMappingURL=plugin.js.map
|
package/dist/raw.js
CHANGED
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The raw-mode renderer: arrow keys and a moving highlight for `select` and `multiselect`,
|
|
3
|
-
* on a terminal that can take them.
|
|
4
|
-
*
|
|
5
|
-
* **It answers the same questions `ask()` does, and returns the same answers.** That is the
|
|
6
|
-
* whole arrangement: line mode is the floor (R5), this sits on top, and a caller chooses
|
|
7
|
-
* between them by asking whether the terminal is one. Anything this can do that line mode
|
|
8
|
-
* cannot is decoration; anything line mode can do that this cannot would be a bug.
|
|
9
|
-
*
|
|
10
|
-
* **On its one dependency.** The design said "spinner from flagstaff"; this imports
|
|
11
|
-
* `closeout` instead, and the difference is the point. A repaint is one escape sequence
|
|
12
|
-
* and belongs here. Hiding the cursor is a global side effect on someone else's terminal,
|
|
13
|
-
* and the obligation it creates — put it back however the process dies — is not a repaint.
|
|
14
|
-
* This file used to own `HIDE_CURSOR`/`SHOW_CURSOR`, the family's third copy, and restore
|
|
15
|
-
* on one path only: the keypress loop, which sees Ctrl-C because raw mode delivers it as a
|
|
16
|
-
* byte. A `SIGINT` from a parent, a `SIGTERM`, a crash or a `process.exit()` elsewhere
|
|
17
|
-
* never reached it, and left the cursor invisible until the user typed `reset` (measured
|
|
18
|
-
* against `dist/raw.js`, 2026-09-15: hide 1, show 0). `hideCursor()` registers the restore
|
|
19
|
-
* in the call that hides, so the two cannot drift.
|
|
20
|
-
*/
|
|
21
1
|
import { hideCursor } from 'closeout/cursor';
|
|
22
2
|
import exitHook from 'closeout/exit-hook';
|
|
23
|
-
import {} from './ask.js';
|
|
24
|
-
import {} from './spec.js';
|
|
25
3
|
const ESC = '\u001B';
|
|
26
4
|
const CSI = `${ESC}[`;
|
|
27
|
-
/** Column 1, up `n` lines, clear to the end of the screen — the only repaint this needs. */
|
|
28
5
|
const erase = (lines) => `${CSI}1G${lines > 1 ? `${CSI}${lines - 1}A` : ''}${CSI}0J`;
|
|
29
|
-
/**
|
|
30
|
-
* What a keypress means. Only the six that drive a list — everything else is `other`, and
|
|
31
|
-
* a widget that does not know what to do with a key does nothing, which is what a person
|
|
32
|
-
* expects from a key they pressed by accident.
|
|
33
|
-
*/
|
|
34
6
|
export function keyOf(data) {
|
|
35
7
|
if (data === `${CSI}A` || data === 'k')
|
|
36
8
|
return 'up';
|
|
@@ -40,26 +12,21 @@ export function keyOf(data) {
|
|
|
40
12
|
return 'space';
|
|
41
13
|
if (data === '\r' || data === '\n')
|
|
42
14
|
return 'enter';
|
|
43
|
-
// Ctrl-C and Ctrl-D. In raw mode the terminal delivers these as bytes rather than
|
|
44
|
-
// signals, so a widget that did not read them would leave a person unable to leave.
|
|
45
15
|
if (data === '\u0003' || data === '\u0004' || data === ESC)
|
|
46
16
|
return 'cancel';
|
|
47
17
|
return 'other';
|
|
48
18
|
}
|
|
49
|
-
/** Whether this runtime can drive the raw renderer at all. */
|
|
50
19
|
export function canRender(keys) {
|
|
51
20
|
return keys.isTTY === true && typeof keys.setRawMode === 'function';
|
|
52
21
|
}
|
|
53
22
|
const MARK = { on: '◉', off: '◯' };
|
|
54
23
|
const POINTER = '❯';
|
|
55
24
|
const CANCELLED = { ok: false, reason: 'cancelled' };
|
|
56
|
-
/** The `◉`/`◯` column, which only a multiselect has. Empty for a single select. */
|
|
57
25
|
function markFor(state, index, multi) {
|
|
58
26
|
if (!multi)
|
|
59
27
|
return '';
|
|
60
28
|
return `${state.selected.has(index) ? MARK.on : MARK.off} `;
|
|
61
29
|
}
|
|
62
|
-
/** One frame of the list. Exported so a test asserts the drawing rather than a screenshot. */
|
|
63
30
|
export function renderList(spec, choices, state, multi) {
|
|
64
31
|
const rows = choices.map((choice, index) => {
|
|
65
32
|
const pointer = index === state.cursor ? POINTER : ' ';
|
|
@@ -69,11 +36,6 @@ export function renderList(spec, choices, state, multi) {
|
|
|
69
36
|
return [spec.message, ...rows].join('\n');
|
|
70
37
|
}
|
|
71
38
|
const clamp = (index, length) => (index + length) % length;
|
|
72
|
-
/**
|
|
73
|
-
* Apply a navigation key, and say whether anything changed — a key with no meaning here
|
|
74
|
-
* changes nothing and repaints nothing, which is what a person expects from a key they
|
|
75
|
-
* pressed by accident.
|
|
76
|
-
*/
|
|
77
39
|
function moved(key, state, length, multi) {
|
|
78
40
|
if (key === 'up') {
|
|
79
41
|
state.cursor = clamp(state.cursor - 1, length);
|
|
@@ -91,29 +53,12 @@ function moved(key, state, length, multi) {
|
|
|
91
53
|
state.selected.add(state.cursor);
|
|
92
54
|
return true;
|
|
93
55
|
}
|
|
94
|
-
/** What enter answers with. List order, not press order: a set of choices has no sequence. */
|
|
95
56
|
function chosen(choices, state, multi) {
|
|
96
57
|
if (!multi)
|
|
97
58
|
return choices[state.cursor]?.value ?? '';
|
|
98
59
|
return [...state.selected].sort((a, b) => a - b).map((index) => choices[index]?.value ?? '');
|
|
99
60
|
}
|
|
100
|
-
/**
|
|
101
|
-
* The writer, seen as a terminal, for `hideCursor()` — which refuses a non-TTY. caique's
|
|
102
|
-
* `Writer` is one method and has no `isTTY`: the terminal test this renderer makes is
|
|
103
|
-
* `canRender(io.keys)`, on the other half of the same terminal, and it has already been
|
|
104
|
-
* made. So it is answered here rather than re-asked of a stream that cannot answer.
|
|
105
|
-
*/
|
|
106
61
|
const asTerminal = (writer) => ({ write: (text) => writer.write(text), isTTY: true });
|
|
107
|
-
/**
|
|
108
|
-
* Drive a list prompt with the arrow keys, repainting in place.
|
|
109
|
-
*
|
|
110
|
-
* Returns the same `Asked` shape `ask()` does, so a caller can swap the two without
|
|
111
|
-
* knowing which ran — and cancels the same way, because `Ctrl-C` in raw mode is a byte and
|
|
112
|
-
* not a signal, and a person who presses it means to leave.
|
|
113
|
-
*
|
|
114
|
-
* The cursor is hidden through `closeout`: the byte path resolves and `restore()` runs in
|
|
115
|
-
* the `finally`; every path that never reaches the `finally` is the exit hook's.
|
|
116
|
-
*/
|
|
117
62
|
export async function askList(spec, io, multi = false) {
|
|
118
63
|
const choices = spec.choices ?? [];
|
|
119
64
|
const state = { cursor: 0, selected: new Set() };
|
|
@@ -125,8 +70,6 @@ export async function askList(spec, io, multi = false) {
|
|
|
125
70
|
};
|
|
126
71
|
io.keys.setRawMode?.(true);
|
|
127
72
|
io.keys.resume?.();
|
|
128
|
-
// Hide and register the restore together. `restore()` shows the cursor and unregisters,
|
|
129
|
-
// so a prompt that ends normally leaves nothing behind for exit to do.
|
|
130
73
|
const restore = hideCursor(asTerminal(io.writer), exitHook);
|
|
131
74
|
paint();
|
|
132
75
|
try {
|
|
@@ -146,8 +89,6 @@ export async function askList(spec, io, multi = false) {
|
|
|
146
89
|
};
|
|
147
90
|
const done = (answer) => {
|
|
148
91
|
io.keys.off('data', onData);
|
|
149
|
-
// Leave the answered question on screen; the `finally` puts the cursor and the
|
|
150
|
-
// terminal back, because a prompt that exits in raw mode leaves the shell unusable.
|
|
151
92
|
io.writer.write(`${erase(painted)}${renderList(spec, choices, state, multi)}\n`);
|
|
152
93
|
resolve(answer);
|
|
153
94
|
};
|
|
@@ -160,4 +101,3 @@ export async function askList(spec, io, multi = false) {
|
|
|
160
101
|
io.keys.pause?.();
|
|
161
102
|
}
|
|
162
103
|
}
|
|
163
|
-
//# sourceMappingURL=raw.js.map
|
package/dist/runtime.js
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The slice of the world caique needs, and the one file here that names `process` (Y9) —
|
|
3
|
-
* the same seam `paratext/src/runtime.ts` declares. `decide()` already took the fields it
|
|
4
|
-
* reads and `createIo()` already took its streams; this is the other half, so a program can
|
|
5
|
-
* get a real one without writing `process.stdin` itself.
|
|
6
|
-
*
|
|
7
|
-
* A **function**, for paratext's reason: a runtime built at import freezes the environment
|
|
8
|
-
* as it was when the module graph loaded, which is before a test can say what it wants.
|
|
9
|
-
* `runtime.test.ts` asks twice across a change, so a captured constant cannot pass.
|
|
10
|
-
*/
|
|
11
|
-
/** What a real process looks like. Callers that have not got one pass their own. */
|
|
12
1
|
export const processRuntime = () => ({
|
|
13
2
|
env: process.env,
|
|
14
3
|
stdin: process.stdin,
|
|
15
4
|
stdout: process.stdout,
|
|
16
5
|
isTTY: { stdin: process.stdin.isTTY === true, stdout: process.stdout.isTTY === true },
|
|
17
6
|
});
|
|
18
|
-
//# sourceMappingURL=runtime.js.map
|