@orkestrel/terminal 0.0.12 → 0.0.14
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 -7
- package/dist/src/core/index.cjs +408 -258
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +544 -297
- package/dist/src/core/index.d.ts +544 -297
- package/dist/src/core/index.js +393 -241
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +187 -201
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +129 -143
- package/dist/src/server/index.d.ts +129 -143
- package/dist/src/server/index.js +176 -185
- package/dist/src/server/index.js.map +1 -1
- package/package.json +23 -19
|
@@ -1,51 +1,44 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _orkestrel_console = require("@orkestrel/console");
|
|
2
3
|
let _src_core = require("../core/index.cjs");
|
|
3
4
|
let _orkestrel_form = require("@orkestrel/form");
|
|
4
|
-
let
|
|
5
|
+
let _orkestrel_console_server = require("@orkestrel/console/server");
|
|
5
6
|
let node_readline = require("node:readline");
|
|
6
7
|
let node_process = require("node:process");
|
|
7
8
|
//#region src/server/constants.ts
|
|
8
|
-
/** The Escape byte (ESC, U+001B) — the lead byte of every CSI cursor-control sequence below. */
|
|
9
|
-
var ESCAPE = String.fromCharCode(27);
|
|
10
|
-
/** The Control Sequence Introducer (`ESC[`) — the prefix of every cursor / erase sequence. */
|
|
11
|
-
var CSI = `[`;
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
10
|
+
* Holds the cursor-UP sequence TEMPLATE (`ESC[{count}A`) — {@link import('./helpers.js').renderCursorUp}
|
|
14
11
|
* interpolates the `{count}` placeholder with the number of lines to climb. Kept as a template so
|
|
15
12
|
* the count stays out of the constant.
|
|
16
13
|
*/
|
|
17
|
-
var CSI_UP = `${CSI}{count}A`;
|
|
14
|
+
var CSI_UP = `${_orkestrel_console.CSI}{count}A`;
|
|
18
15
|
/**
|
|
19
|
-
*
|
|
16
|
+
* Hides the cursor (`ESC[?25l`) — written before the driver starts redrawing a prompt so the cursor
|
|
20
17
|
* does not flicker across the view during an in-place re-render; paired with {@link CURSOR_SHOW}.
|
|
21
18
|
*/
|
|
22
|
-
var CURSOR_HIDE = `${CSI}?25l`;
|
|
23
|
-
/**
|
|
24
|
-
var CURSOR_SHOW = `${CSI}?25h`;
|
|
19
|
+
var CURSOR_HIDE = `${_orkestrel_console.CSI}?25l`;
|
|
20
|
+
/** Shows the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */
|
|
21
|
+
var CURSOR_SHOW = `${_orkestrel_console.CSI}?25h`;
|
|
25
22
|
/**
|
|
26
|
-
*
|
|
23
|
+
* Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the WHOLE previous (possibly
|
|
27
24
|
* multi-line `select` / `checkbox`) view in one write before the new view is rendered, so a redraw
|
|
28
25
|
* never leaves orphaned rows below.
|
|
29
26
|
*/
|
|
30
|
-
var CLEAR_DOWN = `${CSI}J`;
|
|
31
|
-
/** A carriage return (`\r`, U+000D) — returns the cursor to column 0 so a redraw starts at the line's left edge. */
|
|
32
|
-
var CARRIAGE_RETURN = String.fromCharCode(13);
|
|
33
|
-
/** A line feed (`\n`, U+000A) — the line terminator the driver writes after the final committed prompt view. */
|
|
34
|
-
var LINE_FEED = String.fromCharCode(10);
|
|
27
|
+
var CLEAR_DOWN = `${_orkestrel_console.CSI}J`;
|
|
35
28
|
/**
|
|
36
|
-
*
|
|
29
|
+
* Holds the numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback
|
|
37
30
|
* appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are
|
|
38
31
|
* printed numbered and the user types one number on a single readline line.
|
|
39
32
|
*/
|
|
40
33
|
var FALLBACK_SELECT_HINT = "Enter a number";
|
|
41
|
-
/**
|
|
34
|
+
/** Holds the comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */
|
|
42
35
|
var FALLBACK_CHECKBOX_HINT = "Enter numbers separated by commas";
|
|
43
|
-
/**
|
|
36
|
+
/** Holds the hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */
|
|
44
37
|
var FALLBACK_EDITOR_HINT = "(end of input finishes)";
|
|
45
|
-
/**
|
|
38
|
+
/** Holds the hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */
|
|
46
39
|
var FALLBACK_CONFIRM_HINT = "(y/n)";
|
|
47
40
|
/**
|
|
48
|
-
*
|
|
41
|
+
* Holds the format cue appended to a field's label for each control the walk reads as a line of text —
|
|
49
42
|
* the terminal has no date picker, no color well, and no file chooser, so the accepted shape is
|
|
50
43
|
* stated instead. A control with no entry needs none: `text` and `editor` accept any line,
|
|
51
44
|
* `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by
|
|
@@ -59,16 +52,16 @@ var CONTROL_HINTS = Object.freeze({
|
|
|
59
52
|
color: "(#rrggbb)",
|
|
60
53
|
file: "(path)"
|
|
61
54
|
});
|
|
62
|
-
/**
|
|
55
|
+
/** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
|
|
63
56
|
var FILE_HINT = "One path per line, blank to finish";
|
|
64
|
-
/**
|
|
57
|
+
/** Holds the lead on the line listing an open `select`'s offered values, which a typed answer may ignore. */
|
|
65
58
|
var SUGGESTION_LEAD = "Suggestions";
|
|
66
|
-
/**
|
|
59
|
+
/** Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a reader sees why one is missing from the list below. */
|
|
67
60
|
var UNAVAILABLE_LEAD = "Unavailable";
|
|
68
|
-
/**
|
|
61
|
+
/** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
|
|
69
62
|
var LOCKED_MARK = "(locked)";
|
|
70
63
|
/**
|
|
71
|
-
*
|
|
64
|
+
* States what a field is told when the walk read an answer the control cannot hold — a word typed into a
|
|
72
65
|
* `number`, an off-list value typed into an open `select` whose choice is refused. The value binds
|
|
73
66
|
* as absence and this message is invalidated onto the field, so the walk re-asks it with the reason
|
|
74
67
|
* on screen.
|
|
@@ -77,8 +70,8 @@ var REFUSAL_MESSAGE = "Enter a value this field accepts";
|
|
|
77
70
|
//#endregion
|
|
78
71
|
//#region src/server/helpers.ts
|
|
79
72
|
/**
|
|
80
|
-
*
|
|
81
|
-
* `'data'` subscription methods. A total type guard
|
|
73
|
+
* Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` / `off`
|
|
74
|
+
* `'data'` subscription methods. A total type guard: it NEVER throws and returns `false`
|
|
82
75
|
* for anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,
|
|
83
76
|
* or a fake TTY a test injects) to the exact slice the driver reads — no `as`.
|
|
84
77
|
*
|
|
@@ -89,25 +82,13 @@ var REFUSAL_MESSAGE = "Enter a value this field accepts";
|
|
|
89
82
|
* fallback rather than raw mode.
|
|
90
83
|
*
|
|
91
84
|
* @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
|
|
92
|
-
* @returns
|
|
85
|
+
* @returns True if `value` has callable `on` and `off`; false otherwise
|
|
93
86
|
*/
|
|
94
87
|
function isInputStream(value) {
|
|
95
88
|
return typeof value === "object" && value !== null && "on" in value && typeof value.on === "function" && "off" in value && typeof value.off === "function";
|
|
96
89
|
}
|
|
97
90
|
/**
|
|
98
|
-
*
|
|
99
|
-
* total type guard (AGENTS §14): it NEVER throws and returns `false` for anything off-shape, so it
|
|
100
|
-
* narrows the output boundary (the real `process.stdout`, or a recording fake a test injects) to the
|
|
101
|
-
* one method the driver writes through — no `as`.
|
|
102
|
-
*
|
|
103
|
-
* @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
|
|
104
|
-
* @returns `true` when `value` has a callable `write`
|
|
105
|
-
*/
|
|
106
|
-
function isOutputStream(value) {
|
|
107
|
-
return typeof value === "object" && value !== null && "write" in value && typeof value.write === "function";
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard (AGENTS §14)
|
|
91
|
+
* Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard
|
|
111
92
|
* checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface` requires
|
|
112
93
|
* as its `input`. The non-TTY fallback narrows the resolved input stream through this before handing
|
|
113
94
|
* it to readline (never an `as`), so a real piped `process.stdin` (or a `PassThrough` a test injects)
|
|
@@ -115,26 +96,26 @@ function isOutputStream(value) {
|
|
|
115
96
|
* isn't a full readable.
|
|
116
97
|
*
|
|
117
98
|
* @param value - The resolved input stream (or any value crossing the boundary)
|
|
118
|
-
* @returns
|
|
99
|
+
* @returns True if `value` has the readable methods readline needs; false otherwise
|
|
119
100
|
*/
|
|
120
101
|
function isReadable(value) {
|
|
121
102
|
return typeof value === "object" && value !== null && "read" in value && typeof value.read === "function" && "pipe" in value && typeof value.pipe === "function";
|
|
122
103
|
}
|
|
123
104
|
/**
|
|
124
|
-
*
|
|
125
|
-
* callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to choose its path:
|
|
105
|
+
* Checks whether an input stream can be driven in RAW mode — it both reports `isTTY === true` AND
|
|
106
|
+
* exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to choose its path:
|
|
126
107
|
* `true` ⇒ the interactive raw-mode prompts (arrow-key navigation, live re-render); `false` ⇒ the
|
|
127
108
|
* `node:readline` line-input fallback (a piped / non-terminal stream cannot enter raw mode). Total —
|
|
128
109
|
* never throws.
|
|
129
110
|
*
|
|
130
111
|
* @param input - The resolved {@link InputStreamInterface}
|
|
131
|
-
* @returns
|
|
112
|
+
* @returns True if the stream is a TTY with `setRawMode`; false otherwise
|
|
132
113
|
*/
|
|
133
|
-
function
|
|
114
|
+
function supportsRawMode(input) {
|
|
134
115
|
return input.isTTY === true && typeof input.setRawMode === "function";
|
|
135
116
|
}
|
|
136
117
|
/**
|
|
137
|
-
*
|
|
118
|
+
* Counts the terminal LINES a rendered prompt `view` occupies — one more than its newline count
|
|
138
119
|
* (a view with no newline is a single line; N newlines span N+1 lines). The basis of the in-place
|
|
139
120
|
* re-render: the driver records the line count of the view it just wrote so the next redraw knows how
|
|
140
121
|
* far up to move the cursor before overwriting. Total; an empty string is one (empty) line.
|
|
@@ -148,7 +129,7 @@ function lineCount(view) {
|
|
|
148
129
|
return lines;
|
|
149
130
|
}
|
|
150
131
|
/**
|
|
151
|
-
*
|
|
132
|
+
* Returns the cursor-UP control sequence that moves the cursor up `count` lines (`ESC[{count}A`) — or the
|
|
152
133
|
* empty string when `count` is zero or negative (no movement needed, and `ESC[0A` is a wasted write).
|
|
153
134
|
* The pure step the in-place re-render uses to climb back over the previous view before clearing it.
|
|
154
135
|
* Total.
|
|
@@ -156,12 +137,12 @@ function lineCount(view) {
|
|
|
156
137
|
* @param count - How many lines to move the cursor up
|
|
157
138
|
* @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`
|
|
158
139
|
*/
|
|
159
|
-
function
|
|
140
|
+
function renderCursorUp(count) {
|
|
160
141
|
if (count <= 0) return "";
|
|
161
142
|
return `${CSI_UP.replace("{count}", String(count))}`;
|
|
162
143
|
}
|
|
163
144
|
/**
|
|
164
|
-
*
|
|
145
|
+
* Returns the full reposition-and-clear prefix to write BEFORE re-rendering a prompt view in place — given
|
|
165
146
|
* the line count of the PREVIOUS view, it moves the cursor up over those lines, returns it to column
|
|
166
147
|
* 0, and erases everything from there to the end of the screen, so the next view is drawn on a clean
|
|
167
148
|
* region (no orphaned rows from a taller previous view). Pure; the driver writes this immediately
|
|
@@ -178,15 +159,15 @@ function moveUp(count) {
|
|
|
178
159
|
* @returns The control-sequence prefix to write before the new view
|
|
179
160
|
*/
|
|
180
161
|
function redrawPrefix(previousLines) {
|
|
181
|
-
return `${
|
|
162
|
+
return `${renderCursorUp(previousLines - 1)}${_src_core.RETURN}${CLEAR_DOWN}`;
|
|
182
163
|
}
|
|
183
164
|
/**
|
|
184
|
-
*
|
|
185
|
-
* takes — `text` itself, and the
|
|
165
|
+
* Projects any field the walk reads as a LINE OF TEXT into the {@link TextField} the text reducer
|
|
166
|
+
* takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`,
|
|
186
167
|
* `time`, `datetime`, `color`, and one `file` entry. The label carries that control's format cue
|
|
187
168
|
* from {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
|
|
188
169
|
* projection carries no rule, because the AUTHORITATIVE form still evaluates the answer this line
|
|
189
|
-
* binds; it exists only so one reducer
|
|
170
|
+
* binds; it exists only so one reducer covers every one of them.
|
|
190
171
|
*
|
|
191
172
|
* @param field - The field being read
|
|
192
173
|
* @returns The text field the reducer renders for it
|
|
@@ -210,7 +191,7 @@ function fieldToText(field) {
|
|
|
210
191
|
};
|
|
211
192
|
}
|
|
212
193
|
/**
|
|
213
|
-
*
|
|
194
|
+
* Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as
|
|
214
195
|
* `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders
|
|
215
196
|
* as nothing, because a locked field nobody has answered has nothing to show.
|
|
216
197
|
*
|
|
@@ -225,34 +206,42 @@ function valueToText(value) {
|
|
|
225
206
|
return value.join(", ");
|
|
226
207
|
}
|
|
227
208
|
/**
|
|
228
|
-
*
|
|
229
|
-
* value at every door, including a fill, so the walk never puts one in front of the
|
|
230
|
-
* with {@link
|
|
209
|
+
* Returns the choices a `select` or `checkbox` field actually OFFERS — the form refuses a disabled
|
|
210
|
+
* choice's value at every door, including a fill, so the walk never puts one in front of the
|
|
211
|
+
* cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.
|
|
231
212
|
*
|
|
232
213
|
* @param choices - The field's declared choices
|
|
233
214
|
* @returns The choices the walk offers, in declared order
|
|
234
215
|
*/
|
|
235
|
-
function
|
|
216
|
+
function filterEnabled(choices) {
|
|
236
217
|
return choices.filter((choice) => choice.disabled !== true);
|
|
237
218
|
}
|
|
238
219
|
/**
|
|
239
|
-
*
|
|
240
|
-
* {@link
|
|
241
|
-
* declared choice is missing from it.
|
|
220
|
+
* Returns the choices a `select` or `checkbox` field SHOWS but refuses — the complement of
|
|
221
|
+
* {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees
|
|
222
|
+
* why a declared choice is missing from it.
|
|
242
223
|
*
|
|
243
224
|
* @param choices - The field's declared choices
|
|
244
225
|
* @returns The refused choices, in declared order
|
|
245
226
|
*/
|
|
246
|
-
function
|
|
227
|
+
function filterDisabled(choices) {
|
|
247
228
|
return choices.filter((choice) => choice.disabled === true);
|
|
248
229
|
}
|
|
249
|
-
/**
|
|
250
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Renders the section header the walk writes when it enters a new field group, painted by the
|
|
232
|
+
* `message` role.
|
|
233
|
+
*
|
|
234
|
+
* @param styler - The console styler that renders each role
|
|
235
|
+
* @param theme - The resolved prompt theme
|
|
236
|
+
* @param label - The group's declared label, falling back to its own name
|
|
237
|
+
* @returns The rendered section header
|
|
238
|
+
*/
|
|
239
|
+
function renderGroupHeader(styler, theme, label) {
|
|
251
240
|
return styler.render(theme.roles.message, label);
|
|
252
241
|
}
|
|
253
242
|
/**
|
|
254
|
-
*
|
|
255
|
-
* form already holds. The walk writes this instead of a prompt, because the field is still
|
|
243
|
+
* Renders the read-only line a LOCKED field shows — its label, the {@link LOCKED_MARK}, and the
|
|
244
|
+
* answer the form already holds. The walk writes this instead of a prompt, because the field is still
|
|
256
245
|
* validated and still submitted but must not be edited here.
|
|
257
246
|
*
|
|
258
247
|
* @param styler - The console styler that renders each role
|
|
@@ -261,38 +250,54 @@ function groupHeader(styler, theme, label) {
|
|
|
261
250
|
* @param value - The held answer, from {@link valueToText}
|
|
262
251
|
* @returns The rendered line, with no trailing space when there is nothing to show
|
|
263
252
|
*/
|
|
264
|
-
function
|
|
253
|
+
function renderLockedLine(styler, theme, label, value) {
|
|
265
254
|
const head = `${styler.render(theme.roles.muted, theme.icons.dot)} ${styler.render(theme.roles.message, label)} ${styler.render(theme.roles.hint, LOCKED_MARK)}`;
|
|
266
255
|
return value.length === 0 ? head : `${head} ${styler.render(theme.roles.content, value)}`;
|
|
267
256
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
257
|
+
/**
|
|
258
|
+
* Renders the line listing an OPEN select's offered values above its text prompt — a suggestion
|
|
259
|
+
* list, because an open select admits an answer the list does not offer.
|
|
260
|
+
*
|
|
261
|
+
* @param styler - The console styler that renders each role
|
|
262
|
+
* @param theme - The resolved prompt theme
|
|
263
|
+
* @param choices - The choices the open select offers, from {@link filterEnabled}
|
|
264
|
+
* @returns The rendered suggestion line
|
|
265
|
+
*/
|
|
266
|
+
function renderSuggestionLine(styler, theme, choices) {
|
|
270
267
|
const values = choices.map((choice) => choice.value).join(", ");
|
|
271
268
|
return styler.render(theme.roles.hint, `${SUGGESTION_LEAD}: ${values}`);
|
|
272
269
|
}
|
|
273
|
-
/**
|
|
274
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Renders the line naming the choices a field shows but refuses, written above the list the walk
|
|
272
|
+
* drives.
|
|
273
|
+
*
|
|
274
|
+
* @param styler - The console styler that renders each role
|
|
275
|
+
* @param theme - The resolved prompt theme
|
|
276
|
+
* @param choices - The refused choices, from {@link filterDisabled}
|
|
277
|
+
* @returns The rendered unavailable line
|
|
278
|
+
*/
|
|
279
|
+
function renderUnavailableLine(styler, theme, choices) {
|
|
275
280
|
const labels = choices.map((choice) => choice.label).join(", ");
|
|
276
281
|
return styler.render(theme.roles.muted, `${UNAVAILABLE_LEAD}: ${labels}`);
|
|
277
282
|
}
|
|
278
283
|
/**
|
|
279
|
-
*
|
|
280
|
-
* keys, so each offered choice is printed with the number the reader types back. One
|
|
281
|
-
* choice, with no trailing newline.
|
|
284
|
+
* Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate
|
|
285
|
+
* with arrow keys, so each offered choice is printed with the number the reader types back. One
|
|
286
|
+
* line per choice, with no trailing newline.
|
|
282
287
|
*
|
|
283
288
|
* @param styler - The console styler that renders each role
|
|
284
289
|
* @param theme - The resolved prompt theme
|
|
285
|
-
* @param choices - The choices the walk offers, from {@link
|
|
290
|
+
* @param choices - The choices the walk offers, from {@link filterEnabled}
|
|
286
291
|
* @returns The rendered list
|
|
287
292
|
*/
|
|
288
|
-
function
|
|
293
|
+
function renderNumberedList(styler, theme, choices) {
|
|
289
294
|
return choices.map((choice, index) => ` ${styler.render(theme.roles.muted, `${String(index + 1)})`)} ${styler.render(theme.roles.content, choice.label)}`).join("\n");
|
|
290
295
|
}
|
|
291
296
|
//#endregion
|
|
292
297
|
//#region src/server/Terminal.ts
|
|
293
298
|
/**
|
|
294
|
-
*
|
|
295
|
-
*
|
|
299
|
+
* Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive form
|
|
300
|
+
* DRIVER, and the only impure part of the terminal stack. {@link ask} walks one
|
|
296
301
|
* form's fields in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching
|
|
297
302
|
* pure reducer, renders each returned view in place, and binds every answer through the form's own
|
|
298
303
|
* `fill`. It owns no form logic: the schema, the rules, the values, and the settlement all belong to
|
|
@@ -301,7 +306,7 @@ function numberedList(styler, theme, choices) {
|
|
|
301
306
|
* @remarks
|
|
302
307
|
* See {@link TerminalInterface} for the driving contract. The walk itself:
|
|
303
308
|
*
|
|
304
|
-
* - **
|
|
309
|
+
* - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, and
|
|
305
310
|
* each `file` entry are read as one line of text through {@link fieldToText}, which appends that
|
|
306
311
|
* control's format cue to the label. `password`, `confirm`, `editor`, `select`, and `checkbox`
|
|
307
312
|
* each drive their own reducer. An open `select` is a suggestion list plus a typed line, because
|
|
@@ -335,7 +340,7 @@ var Terminal = class {
|
|
|
335
340
|
#ended = false;
|
|
336
341
|
constructor(options) {
|
|
337
342
|
this.#input = isInputStream(options?.input) ? options.input : node_process.stdin;
|
|
338
|
-
this.#output =
|
|
343
|
+
this.#output = (0, _orkestrel_console_server.isStreamTarget)(options?.output) ? options.output : node_process.stdout;
|
|
339
344
|
this.#styler = (0, _orkestrel_console.createStyler)();
|
|
340
345
|
this.#theme = (0, _src_core.createPromptTheme)(options?.theme);
|
|
341
346
|
}
|
|
@@ -348,7 +353,7 @@ var Terminal = class {
|
|
|
348
353
|
const result = form.submit();
|
|
349
354
|
if (result.success) return await form.answer;
|
|
350
355
|
this.#report(form, result.error);
|
|
351
|
-
const again = this.#
|
|
356
|
+
const again = this.#collectEditable(form, result.error);
|
|
352
357
|
if (again.length === 0 || this.#ended) {
|
|
353
358
|
form.destroy();
|
|
354
359
|
return await form.answer;
|
|
@@ -360,7 +365,7 @@ var Terminal = class {
|
|
|
360
365
|
}
|
|
361
366
|
}
|
|
362
367
|
/**
|
|
363
|
-
*
|
|
368
|
+
* Asks `fields` in the order given, skipping what the walk must not touch and rendering what it
|
|
364
369
|
* must not edit. Returns early the moment the form stops being editable, so an abandoned form
|
|
365
370
|
* ends the walk between fields as well as under an active read.
|
|
366
371
|
*/
|
|
@@ -372,10 +377,10 @@ var Terminal = class {
|
|
|
372
377
|
if (form.disabled.has(field.name)) continue;
|
|
373
378
|
if (field.group !== group) {
|
|
374
379
|
group = field.group;
|
|
375
|
-
if (group !== void 0) this.#
|
|
380
|
+
if (group !== void 0) this.#writeGroup(form, group);
|
|
376
381
|
}
|
|
377
382
|
if (field.locked === true) {
|
|
378
|
-
this.#
|
|
383
|
+
this.#writeLocked(form, field);
|
|
379
384
|
continue;
|
|
380
385
|
}
|
|
381
386
|
const raw = await this.#read(form, field);
|
|
@@ -383,31 +388,28 @@ var Terminal = class {
|
|
|
383
388
|
this.#bind(form, field, raw);
|
|
384
389
|
}
|
|
385
390
|
}
|
|
386
|
-
/**
|
|
387
|
-
#
|
|
391
|
+
/** Writes a group's label as a section header, resolving the schema's declared label and falling back to the group's own name. */
|
|
392
|
+
#writeGroup(form, name) {
|
|
388
393
|
const label = form.schema.groups?.find((group) => group.name === name)?.label ?? name;
|
|
389
|
-
this.#output.write(`
|
|
390
|
-
${groupHeader(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(label))}
|
|
391
|
-
`);
|
|
394
|
+
this.#output.write(`${_src_core.NEWLINE}${renderGroupHeader(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(label))}${_src_core.NEWLINE}`);
|
|
392
395
|
}
|
|
393
|
-
/**
|
|
394
|
-
#
|
|
395
|
-
const line =
|
|
396
|
-
this.#output.write(`${line}
|
|
397
|
-
`);
|
|
396
|
+
/** Renders a locked field read-only — its label, its mark, and the answer the form already holds. */
|
|
397
|
+
#writeLocked(form, field) {
|
|
398
|
+
const line = renderLockedLine(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(field.label ?? field.name), (0, _src_core.sanitizeDisplayText)(valueToText(form.values[field.name])));
|
|
399
|
+
this.#output.write(`${line}${_src_core.NEWLINE}`);
|
|
398
400
|
}
|
|
399
|
-
/**
|
|
401
|
+
/** Reads one field through the reducer its control names, resolving the raw answer the binding projects. */
|
|
400
402
|
#read(form, field) {
|
|
401
|
-
if (field.control === "password") return this.#
|
|
403
|
+
if (field.control === "password") return this.#askPassword(form, field);
|
|
402
404
|
if (field.control === "confirm") return this.#confirm(form, field);
|
|
403
|
-
if (field.control === "editor") return this.#
|
|
405
|
+
if (field.control === "editor") return this.#askEditor(form, field);
|
|
404
406
|
if (field.control === "select") return this.#select(form, field);
|
|
405
407
|
if (field.control === "checkbox") return this.#checkbox(form, field);
|
|
406
408
|
if (field.control === "file") return this.#file(form, field);
|
|
407
|
-
return this.#
|
|
409
|
+
return this.#askText(form, field);
|
|
408
410
|
}
|
|
409
411
|
/**
|
|
410
|
-
*
|
|
412
|
+
* Binds one raw answer to the form — the single binding, and the only place this driver writes a
|
|
411
413
|
* value. `parseValue` coerces the raw answer to the control's own shape and `matchesAnswer`
|
|
412
414
|
* projects a blank one to absence, which is what keeps `required` refusing a bare return. A raw
|
|
413
415
|
* answer the control cannot hold binds as absence and invalidates the field, so the field comes
|
|
@@ -419,8 +421,8 @@ ${groupHeader(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(labe
|
|
|
419
421
|
form.fill(field.name, (0, _orkestrel_form.matchesAnswer)(value) ? value : void 0);
|
|
420
422
|
if (value === void 0 && (0, _orkestrel_form.matchesAnswer)(raw)) form.invalidate(field.name, REFUSAL_MESSAGE);
|
|
421
423
|
}
|
|
422
|
-
/**
|
|
423
|
-
#
|
|
424
|
+
/** Collects the erroring fields the walk can ask again — every field the walk skips or renders read-only is excluded, because a second pass could not change its answer. */
|
|
425
|
+
#collectEditable(form, errors) {
|
|
424
426
|
const fields = [];
|
|
425
427
|
for (const error of errors) {
|
|
426
428
|
const field = form.field(error.field);
|
|
@@ -432,37 +434,36 @@ ${groupHeader(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(labe
|
|
|
432
434
|
}
|
|
433
435
|
return fields;
|
|
434
436
|
}
|
|
435
|
-
/**
|
|
437
|
+
/** Writes every supplied failure against its field's label. */
|
|
436
438
|
#report(form, errors) {
|
|
437
439
|
for (const error of errors) {
|
|
438
440
|
const label = (0, _src_core.sanitizeDisplayText)(form.field(error.field)?.label ?? error.field);
|
|
439
441
|
const message = (0, _src_core.sanitizeDisplayText)(error.message);
|
|
440
|
-
const line = (0, _src_core.
|
|
441
|
-
this.#output.write(`${line}
|
|
442
|
-
`);
|
|
442
|
+
const line = (0, _src_core.renderErrorLine)(this.#styler, this.#theme, `${label}: ${message}`);
|
|
443
|
+
this.#output.write(`${line}${_src_core.NEWLINE}`);
|
|
443
444
|
}
|
|
444
445
|
}
|
|
445
|
-
/**
|
|
446
|
-
#
|
|
446
|
+
/** Reads a field as one line of text — `text` itself and the controls a terminal has no widget for. */
|
|
447
|
+
#askText(form, field) {
|
|
447
448
|
const state = (0, _src_core.createInputState)(fieldToText(field), this.#styler, this.#theme);
|
|
448
|
-
if (
|
|
449
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reduceInput);
|
|
449
450
|
return this.#line(form, state.message, state.default);
|
|
450
451
|
}
|
|
451
|
-
/**
|
|
452
|
-
#
|
|
452
|
+
/** Reads a secret — masked live in raw mode, and read without echo through readline on a stream that cannot enter it. */
|
|
453
|
+
#askPassword(form, field) {
|
|
453
454
|
const state = (0, _src_core.createPasswordState)(field, this.#styler, this.#theme);
|
|
454
|
-
if (
|
|
455
|
-
return this.#prompt(form, (0, _src_core.
|
|
455
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reducePassword);
|
|
456
|
+
return this.#prompt(form, (0, _src_core.renderPromptHeader)(this.#styler, this.#theme, state.message));
|
|
456
457
|
}
|
|
457
458
|
/**
|
|
458
|
-
*
|
|
459
|
+
* Reads a yes or no. The fallback accepts `y` / `yes` / `n` / `no` in any case and takes the
|
|
459
460
|
* field's default for a bare line; anything else is returned as typed, so the binding refuses it
|
|
460
461
|
* and the walk asks again rather than silently reading it as no.
|
|
461
462
|
*/
|
|
462
463
|
async #confirm(form, field) {
|
|
463
464
|
const state = (0, _src_core.createConfirmState)(field, this.#styler, this.#theme);
|
|
464
|
-
if (
|
|
465
|
-
const header = (0, _src_core.
|
|
465
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reduceConfirm);
|
|
466
|
+
const header = (0, _src_core.renderHintedHeader)(this.#styler, this.#theme, state.message, FALLBACK_CONFIRM_HINT);
|
|
466
467
|
const line = await this.#prompt(form, header);
|
|
467
468
|
const answer = line.trim().toLowerCase();
|
|
468
469
|
if (answer.length === 0) return state.default;
|
|
@@ -470,96 +471,91 @@ ${groupHeader(this.#styler, this.#theme, (0, _src_core.sanitizeDisplayText)(labe
|
|
|
470
471
|
if (answer === "n" || answer === "no") return false;
|
|
471
472
|
return line.trim();
|
|
472
473
|
}
|
|
473
|
-
/**
|
|
474
|
-
#
|
|
474
|
+
/** Reads text over many lines — ctrl-d finishes in raw mode, and end of input finishes on a piped stream. */
|
|
475
|
+
#askEditor(form, field) {
|
|
475
476
|
const state = (0, _src_core.createEditorState)(field, this.#styler, this.#theme);
|
|
476
|
-
if (
|
|
477
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reduceEditor);
|
|
477
478
|
return this.#block(form, state.message, state.default);
|
|
478
479
|
}
|
|
479
480
|
/**
|
|
480
|
-
*
|
|
481
|
+
* Reads one choice. A disabled choice is named above the list and never offered, because the form
|
|
481
482
|
* refuses its value at every door. An open select is a suggestion list plus a typed line, since
|
|
482
483
|
* `open` admits an answer the list does not offer; a closed select with nothing left to offer
|
|
483
484
|
* resolves absence and lets the form's own rules report it.
|
|
484
485
|
*/
|
|
485
486
|
async #select(form, field) {
|
|
486
|
-
const choices =
|
|
487
|
-
this.#
|
|
487
|
+
const choices = filterEnabled(field.choices);
|
|
488
|
+
this.#writeUnavailable(field.choices);
|
|
488
489
|
if (field.open === true) {
|
|
489
490
|
if (choices.length > 0) {
|
|
490
491
|
const display = choices.map((choice) => ({
|
|
491
492
|
...choice,
|
|
492
493
|
value: (0, _src_core.sanitizeDisplayText)(choice.value)
|
|
493
494
|
}));
|
|
494
|
-
this.#output.write(`${
|
|
495
|
-
`);
|
|
495
|
+
this.#output.write(`${renderSuggestionLine(this.#styler, this.#theme, display)}${_src_core.NEWLINE}`);
|
|
496
496
|
}
|
|
497
|
-
return this.#
|
|
497
|
+
return this.#askText(form, field);
|
|
498
498
|
}
|
|
499
499
|
if (choices.length === 0) return void 0;
|
|
500
500
|
const state = (0, _src_core.createSelectState)({
|
|
501
501
|
...field,
|
|
502
502
|
choices
|
|
503
503
|
}, this.#styler, this.#theme);
|
|
504
|
-
if (
|
|
505
|
-
this.#
|
|
506
|
-
const line = await this.#prompt(form, this.#
|
|
504
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reduceSelect);
|
|
505
|
+
this.#writeList(state.message, choices);
|
|
506
|
+
const line = await this.#prompt(form, this.#formatHint(`${FALLBACK_SELECT_HINT}:`));
|
|
507
507
|
return choices[Number.parseInt(line.trim(), 10) - 1]?.value;
|
|
508
508
|
}
|
|
509
509
|
/**
|
|
510
|
-
*
|
|
510
|
+
* Reads any number of choices. Disabled choices are named above the list and never offered, so a
|
|
511
511
|
* box the form would refuse can never be ticked. An empty answer is an answered "none of them",
|
|
512
512
|
* which is what `matchesAnswer` says an empty list means.
|
|
513
513
|
*/
|
|
514
514
|
async #checkbox(form, field) {
|
|
515
|
-
const choices =
|
|
516
|
-
this.#
|
|
515
|
+
const choices = filterEnabled(field.choices);
|
|
516
|
+
this.#writeUnavailable(field.choices);
|
|
517
517
|
const state = (0, _src_core.createCheckboxState)({
|
|
518
518
|
...field,
|
|
519
519
|
choices
|
|
520
520
|
}, this.#styler, this.#theme);
|
|
521
|
-
if (
|
|
522
|
-
this.#
|
|
523
|
-
return (await this.#prompt(form, this.#
|
|
521
|
+
if (supportsRawMode(this.#input)) return this.#drive(form, state, _src_core.reduceCheckbox);
|
|
522
|
+
this.#writeList(state.message, choices);
|
|
523
|
+
return (await this.#prompt(form, this.#formatHint(`${FALLBACK_CHECKBOX_HINT}:`))).split(",").map((part) => choices[Number.parseInt(part.trim(), 10) - 1]?.value).filter((value) => value !== void 0);
|
|
524
524
|
}
|
|
525
525
|
/**
|
|
526
|
-
*
|
|
526
|
+
* Reads file paths — names only, because bytes never enter a form. A `multiple` field collects one
|
|
527
527
|
* path per line until a blank one, and a single field takes one. No path at all is absence rather
|
|
528
528
|
* than an empty list, because the reader answered a blank line.
|
|
529
529
|
*/
|
|
530
530
|
async #file(form, field) {
|
|
531
|
-
if (field.multiple === true) this.#output.write(`${this.#
|
|
532
|
-
`);
|
|
531
|
+
if (field.multiple === true) this.#output.write(`${this.#formatHint(FILE_HINT)}${_src_core.NEWLINE}`);
|
|
533
532
|
const paths = [];
|
|
534
533
|
for (;;) {
|
|
535
|
-
const entry = await this.#
|
|
534
|
+
const entry = await this.#askText(form, field);
|
|
536
535
|
if (entry.length === 0) break;
|
|
537
536
|
paths.push(entry);
|
|
538
537
|
if (field.multiple !== true || this.#ended) break;
|
|
539
538
|
}
|
|
540
539
|
return paths.length > 0 ? paths : void 0;
|
|
541
540
|
}
|
|
542
|
-
/**
|
|
543
|
-
#
|
|
544
|
-
const refused =
|
|
541
|
+
/** Names the choices a field shows but refuses, above the list the walk drives. */
|
|
542
|
+
#writeUnavailable(choices) {
|
|
543
|
+
const refused = filterDisabled(choices);
|
|
545
544
|
if (refused.length === 0) return;
|
|
546
|
-
this.#output.write(`${
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
/** Paint one line of supplementary instruction through the `hint` role. */
|
|
558
|
-
#hint(text) {
|
|
545
|
+
this.#output.write(`${renderUnavailableLine(this.#styler, this.#theme, refused)}${_src_core.NEWLINE}`);
|
|
546
|
+
}
|
|
547
|
+
/** Writes a field's header above its numbered choice list — the non-TTY presentation of a choice field. */
|
|
548
|
+
#writeList(message, choices) {
|
|
549
|
+
const header = (0, _src_core.renderPromptHeader)(this.#styler, this.#theme, message);
|
|
550
|
+
const list = renderNumberedList(this.#styler, this.#theme, choices);
|
|
551
|
+
this.#output.write(`${header}${_src_core.NEWLINE}${list}${_src_core.NEWLINE}`);
|
|
552
|
+
}
|
|
553
|
+
/** Paints one line of supplementary instruction through the `hint` role. */
|
|
554
|
+
#formatHint(text) {
|
|
559
555
|
return this.#styler.render(this.#theme.roles.hint, text);
|
|
560
556
|
}
|
|
561
557
|
/**
|
|
562
|
-
*
|
|
558
|
+
* Isolates the irreducible Node raw-mode primitive — the ONLY place raw mode is touched. Switches the input
|
|
563
559
|
* into raw mode (each keypress delivered immediately, no echo), resumes its flow, and subscribes
|
|
564
560
|
* `handler` to `'data'`. The paired {@link #leaveRaw} operation unsubscribes the exact handler,
|
|
565
561
|
* leaves raw mode, and pauses the stream on submit, cancel, abandon, or throw.
|
|
@@ -579,7 +575,7 @@ ${list}
|
|
|
579
575
|
this.#input.pause?.();
|
|
580
576
|
}
|
|
581
577
|
/**
|
|
582
|
-
*
|
|
578
|
+
* Drives ONE field over raw-mode stdin — the generic engine every interactive control shares.
|
|
583
579
|
* Renders the reducer's initial view, enters raw mode once, and on each keypress runs `parseKey` →
|
|
584
580
|
* `reduce` → an in-place re-render; on `submit` it cleans up and resolves the reducer's value, on
|
|
585
581
|
* `cancel` (ctrl-c) it cleans up and rejects a {@link TerminalError} coded `CANCEL`. Abandoning
|
|
@@ -595,8 +591,7 @@ ${list}
|
|
|
595
591
|
firstView = reduce(initial, (0, _src_core.parseKey)("")).view;
|
|
596
592
|
this.#output.write(firstView);
|
|
597
593
|
} catch (error) {
|
|
598
|
-
this.#output.write(`${CURSOR_SHOW}
|
|
599
|
-
`);
|
|
594
|
+
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
600
595
|
reject(error);
|
|
601
596
|
return;
|
|
602
597
|
}
|
|
@@ -620,45 +615,41 @@ ${list}
|
|
|
620
615
|
}
|
|
621
616
|
this.#render(step.view, lines);
|
|
622
617
|
this.#leaveRaw(token);
|
|
623
|
-
this.#output.write(`${CURSOR_SHOW}
|
|
624
|
-
`);
|
|
618
|
+
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
625
619
|
if (step.status === "cancel") reject(new _src_core.TerminalError("CANCEL", "Prompt cancelled"));
|
|
626
620
|
else if (step.status === "submit" && step.value !== void 0) resolve(step.value);
|
|
627
621
|
else reject(new _src_core.TerminalError("DRIVER", "submit produced no value"));
|
|
628
622
|
} catch (error) {
|
|
629
623
|
this.#leaveRaw(token);
|
|
630
|
-
this.#output.write(`${CURSOR_SHOW}
|
|
631
|
-
`);
|
|
624
|
+
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
632
625
|
reject(error);
|
|
633
626
|
}
|
|
634
627
|
};
|
|
635
628
|
}
|
|
636
|
-
/**
|
|
629
|
+
/** Ends an active read that the form outlived — releases raw mode exactly as a submit does, then fails the read with the form's own error. */
|
|
637
630
|
#interrupt(token, reject, error) {
|
|
638
631
|
if (!this.#handlers.has(token)) return;
|
|
639
632
|
this.#leaveRaw(token);
|
|
640
|
-
this.#output.write(`${CURSOR_SHOW}
|
|
641
|
-
`);
|
|
633
|
+
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
642
634
|
reject(error);
|
|
643
635
|
}
|
|
644
|
-
/**
|
|
636
|
+
/** Redraws a field view in place — climbs over the previous view's `previousLines`, clears, and writes the new view (the pure cursor-math is {@link redrawPrefix}). */
|
|
645
637
|
#render(view, previousLines) {
|
|
646
638
|
this.#output.write(`${redrawPrefix(previousLines)}${view}`);
|
|
647
639
|
}
|
|
648
|
-
/**
|
|
640
|
+
/** Reads one line for a text-shaped field, taking the field's default when the line is bare. */
|
|
649
641
|
async #line(form, message, seed) {
|
|
650
|
-
const answer = await this.#prompt(form, (0, _src_core.
|
|
642
|
+
const answer = await this.#prompt(form, (0, _src_core.renderPromptHeader)(this.#styler, this.#theme, message));
|
|
651
643
|
return answer.length > 0 ? answer : seed;
|
|
652
644
|
}
|
|
653
645
|
/**
|
|
654
|
-
*
|
|
646
|
+
* Reads a whole block for an `editor` field — a piped stream has no ctrl-d keypress, so end of
|
|
655
647
|
* input is the terminator. It drains every remaining line, which is why nothing after an `editor`
|
|
656
648
|
* field can be asked on the same stream.
|
|
657
649
|
*/
|
|
658
650
|
async #block(form, message, seed) {
|
|
659
|
-
const header = (0, _src_core.
|
|
660
|
-
this.#output.write(`${header}
|
|
661
|
-
`);
|
|
651
|
+
const header = (0, _src_core.renderHintedHeader)(this.#styler, this.#theme, message, FALLBACK_EDITOR_HINT);
|
|
652
|
+
this.#output.write(`${header}${_src_core.NEWLINE}`);
|
|
662
653
|
const collected = [];
|
|
663
654
|
for (;;) {
|
|
664
655
|
const line = await this.#next(form);
|
|
@@ -668,13 +659,13 @@ ${list}
|
|
|
668
659
|
const text = collected.join("\n");
|
|
669
660
|
return text.length > 0 ? text : seed;
|
|
670
661
|
}
|
|
671
|
-
/**
|
|
662
|
+
/** Writes one field's header and reads back the line the reader answers it with, or `''` once the stream has ended. */
|
|
672
663
|
async #prompt(form, header) {
|
|
673
664
|
this.#output.write(`${header} `);
|
|
674
665
|
return await this.#next(form) ?? "";
|
|
675
666
|
}
|
|
676
667
|
/**
|
|
677
|
-
*
|
|
668
|
+
* Takes the next line the input carries — from the buffer when the reader has already read ahead,
|
|
678
669
|
* and otherwise by waiting for one. It resolves ABSENCE once the stream has ended, so a walk over
|
|
679
670
|
* an exhausted stream settles instead of waiting forever, and it rejects when the form is
|
|
680
671
|
* abandoned, so an unanswerable read ends with the form rather than outliving it.
|
|
@@ -683,7 +674,7 @@ ${list}
|
|
|
683
674
|
const buffered = this.#queue.shift();
|
|
684
675
|
if (buffered !== void 0) return Promise.resolve(buffered);
|
|
685
676
|
if (this.#ended) return Promise.resolve(void 0);
|
|
686
|
-
this.#
|
|
677
|
+
this.#startReader();
|
|
687
678
|
return new Promise((resolve, reject) => {
|
|
688
679
|
this.#taker = resolve;
|
|
689
680
|
form.answer.catch((error) => {
|
|
@@ -694,19 +685,19 @@ ${list}
|
|
|
694
685
|
});
|
|
695
686
|
}
|
|
696
687
|
/**
|
|
697
|
-
*
|
|
688
|
+
* Opens the walk's ONE readline interface, or keeps the open one. A whole form is many questions
|
|
698
689
|
* over one stream, and an interface reads ahead: a fresh interface per question would swallow
|
|
699
690
|
* every line it read past the one it was asked for, so the walk holds a single reader and buffers
|
|
700
691
|
* what arrives early. It is closed when the walk ends, so the process is free to exit.
|
|
701
692
|
*/
|
|
702
|
-
#
|
|
693
|
+
#startReader() {
|
|
703
694
|
if (this.#interface !== void 0) return;
|
|
704
|
-
const rl = (0, node_readline.createInterface)(this.#
|
|
695
|
+
const rl = (0, node_readline.createInterface)(this.#openReadline());
|
|
705
696
|
rl.on("line", (line) => this.#accept(line));
|
|
706
697
|
rl.on("close", () => this.#finish());
|
|
707
698
|
this.#interface = rl;
|
|
708
699
|
}
|
|
709
|
-
/**
|
|
700
|
+
/** Hands a line to whoever is waiting for it, or buffers it for the next question. */
|
|
710
701
|
#accept(line) {
|
|
711
702
|
const taker = this.#taker;
|
|
712
703
|
if (taker === void 0) {
|
|
@@ -716,7 +707,7 @@ ${list}
|
|
|
716
707
|
this.#taker = void 0;
|
|
717
708
|
taker(line);
|
|
718
709
|
}
|
|
719
|
-
/**
|
|
710
|
+
/** Records that no further line can arrive and releases whoever was waiting for one. */
|
|
720
711
|
#finish() {
|
|
721
712
|
this.#ended = true;
|
|
722
713
|
const taker = this.#taker;
|
|
@@ -724,7 +715,7 @@ ${list}
|
|
|
724
715
|
this.#taker = void 0;
|
|
725
716
|
taker(void 0);
|
|
726
717
|
}
|
|
727
|
-
/**
|
|
718
|
+
/** Closes the walk's reader and releases a waiting question — the walk is over, so the stream must stop holding the process open. */
|
|
728
719
|
#close() {
|
|
729
720
|
const taker = this.#taker;
|
|
730
721
|
this.#taker = void 0;
|
|
@@ -736,14 +727,14 @@ ${list}
|
|
|
736
727
|
rl.close();
|
|
737
728
|
}
|
|
738
729
|
/**
|
|
739
|
-
*
|
|
730
|
+
* Narrows the resolved input to the `node:readline` `createInterface` boundary, never an assertion.
|
|
740
731
|
* The fallback only runs on a real piped `process.stdin` (or a `PassThrough` a test injects), both
|
|
741
732
|
* genuine readables; a minimal non-readable fake reaching here means the walk was driven with a
|
|
742
733
|
* stream it cannot use, which fails loudly rather than silently. `terminal: false` leaves readline
|
|
743
734
|
* as a line decoder and nothing else: the walk writes every prompt itself, through the same output
|
|
744
735
|
* every other line goes to, so nothing is written twice and a secret is never echoed.
|
|
745
736
|
*/
|
|
746
|
-
#
|
|
737
|
+
#openReadline() {
|
|
747
738
|
const input = this.#input;
|
|
748
739
|
if (!isReadable(input)) throw new _src_core.TerminalError("DRIVER", "Terminal fallback requires a readable input stream");
|
|
749
740
|
return {
|
|
@@ -755,7 +746,7 @@ ${list}
|
|
|
755
746
|
//#endregion
|
|
756
747
|
//#region src/server/factories.ts
|
|
757
748
|
/**
|
|
758
|
-
*
|
|
749
|
+
* Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio, beside
|
|
759
750
|
* the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the broker
|
|
760
751
|
* PARKS a live form until somebody elsewhere answers it, a `Terminal` answers one HERE: it walks the
|
|
761
752
|
* form's fields in schema order, drives each control's pure reducer over raw-mode stdin, binds every
|
|
@@ -768,7 +759,7 @@ ${list}
|
|
|
768
759
|
* @remarks
|
|
769
760
|
* - **The form is the unit.** `ask` takes the caller's live form and returns its settled values. The
|
|
770
761
|
* returned promise is that form's own `answer`, so a caller holding the form can await either one.
|
|
771
|
-
* - **Every control renders.**
|
|
762
|
+
* - **Every control renders.** The line-read controls are read as a line of text with their format cue,
|
|
772
763
|
* `password` masks, `confirm` takes a key, `editor` takes a block, `select` and `checkbox` drive a
|
|
773
764
|
* list, and an open `select` accepts a value its list does not offer.
|
|
774
765
|
* - **The form decides.** A blank answer binds as absence, the form evaluates, and a refusal
|
|
@@ -801,41 +792,36 @@ function createTerminal(options) {
|
|
|
801
792
|
return new Terminal(options);
|
|
802
793
|
}
|
|
803
794
|
//#endregion
|
|
804
|
-
exports.CARRIAGE_RETURN = CARRIAGE_RETURN;
|
|
805
795
|
exports.CLEAR_DOWN = CLEAR_DOWN;
|
|
806
796
|
exports.CONTROL_HINTS = CONTROL_HINTS;
|
|
807
|
-
exports.CSI = CSI;
|
|
808
797
|
exports.CSI_UP = CSI_UP;
|
|
809
798
|
exports.CURSOR_HIDE = CURSOR_HIDE;
|
|
810
799
|
exports.CURSOR_SHOW = CURSOR_SHOW;
|
|
811
|
-
exports.ESCAPE = ESCAPE;
|
|
812
800
|
exports.FALLBACK_CHECKBOX_HINT = FALLBACK_CHECKBOX_HINT;
|
|
813
801
|
exports.FALLBACK_CONFIRM_HINT = FALLBACK_CONFIRM_HINT;
|
|
814
802
|
exports.FALLBACK_EDITOR_HINT = FALLBACK_EDITOR_HINT;
|
|
815
803
|
exports.FALLBACK_SELECT_HINT = FALLBACK_SELECT_HINT;
|
|
816
804
|
exports.FILE_HINT = FILE_HINT;
|
|
817
|
-
exports.LINE_FEED = LINE_FEED;
|
|
818
805
|
exports.LOCKED_MARK = LOCKED_MARK;
|
|
819
806
|
exports.REFUSAL_MESSAGE = REFUSAL_MESSAGE;
|
|
820
807
|
exports.SUGGESTION_LEAD = SUGGESTION_LEAD;
|
|
821
808
|
exports.Terminal = Terminal;
|
|
822
809
|
exports.UNAVAILABLE_LEAD = UNAVAILABLE_LEAD;
|
|
823
810
|
exports.createTerminal = createTerminal;
|
|
824
|
-
exports.disabledChoices = disabledChoices;
|
|
825
|
-
exports.enabledChoices = enabledChoices;
|
|
826
811
|
exports.fieldToText = fieldToText;
|
|
827
|
-
exports.
|
|
812
|
+
exports.filterDisabled = filterDisabled;
|
|
813
|
+
exports.filterEnabled = filterEnabled;
|
|
828
814
|
exports.isInputStream = isInputStream;
|
|
829
|
-
exports.isOutputStream = isOutputStream;
|
|
830
815
|
exports.isReadable = isReadable;
|
|
831
816
|
exports.lineCount = lineCount;
|
|
832
|
-
exports.lockedLine = lockedLine;
|
|
833
|
-
exports.moveUp = moveUp;
|
|
834
|
-
exports.numberedList = numberedList;
|
|
835
|
-
exports.rawCapable = rawCapable;
|
|
836
817
|
exports.redrawPrefix = redrawPrefix;
|
|
837
|
-
exports.
|
|
838
|
-
exports.
|
|
818
|
+
exports.renderCursorUp = renderCursorUp;
|
|
819
|
+
exports.renderGroupHeader = renderGroupHeader;
|
|
820
|
+
exports.renderLockedLine = renderLockedLine;
|
|
821
|
+
exports.renderNumberedList = renderNumberedList;
|
|
822
|
+
exports.renderSuggestionLine = renderSuggestionLine;
|
|
823
|
+
exports.renderUnavailableLine = renderUnavailableLine;
|
|
824
|
+
exports.supportsRawMode = supportsRawMode;
|
|
839
825
|
exports.valueToText = valueToText;
|
|
840
826
|
|
|
841
827
|
//# sourceMappingURL=index.cjs.map
|