@orkestrel/terminal 0.0.19 → 0.0.21
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/dist/src/core/index.cjs +53 -45
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +3 -0
- package/dist/src/core/index.d.ts +3 -0
- package/dist/src/core/index.js +54 -46
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +81 -39
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +10 -6
- package/dist/src/server/index.d.ts +10 -6
- package/dist/src/server/index.js +82 -41
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -20
|
@@ -6,7 +6,7 @@ let _orkestrel_form = require("@orkestrel/form");
|
|
|
6
6
|
let _orkestrel_console_server = require("@orkestrel/console/server");
|
|
7
7
|
let node_readline = require("node:readline");
|
|
8
8
|
let node_process = require("node:process");
|
|
9
|
-
|
|
9
|
+
// src/server/constants.ts
|
|
10
10
|
/**
|
|
11
11
|
* Holds the cursor-up sequence template (`ESC[{count}A`) —
|
|
12
12
|
* {@link import('./helpers.js').renderCursorUp} interpolates the `{count}` placeholder with the
|
|
@@ -51,10 +51,13 @@ var CONTROL_HINTS = Object.freeze({
|
|
|
51
51
|
time: "(HH:MM)",
|
|
52
52
|
datetime: "(YYYY-MM-DDTHH:MM)",
|
|
53
53
|
color: "(#rrggbb)",
|
|
54
|
+
list: "(entry)",
|
|
54
55
|
file: "(path)"
|
|
55
56
|
});
|
|
56
57
|
/** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
|
|
57
58
|
var FILE_HINT = "One path per line, blank to finish";
|
|
59
|
+
/** Holds the instruction a `list` field shows before its entries — one entry per line, and a blank line ends the list. */
|
|
60
|
+
var LIST_HINT = "One entry per line, blank to finish";
|
|
58
61
|
/**
|
|
59
62
|
* Holds the lead on the line listing an open `select`'s offered values, which a typed answer can
|
|
60
63
|
* ignore.
|
|
@@ -74,8 +77,7 @@ var LOCKED_MARK = "(locked)";
|
|
|
74
77
|
* on screen.
|
|
75
78
|
*/
|
|
76
79
|
var REFUSAL_MESSAGE = "Enter a value this field accepts";
|
|
77
|
-
|
|
78
|
-
//#region src/server/helpers.ts
|
|
80
|
+
// src/server/helpers.ts
|
|
79
81
|
/**
|
|
80
82
|
* Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /
|
|
81
83
|
* `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for
|
|
@@ -131,9 +133,7 @@ function supportsRawMode(input) {
|
|
|
131
133
|
* @returns The number of lines the view spans (always at least 1)
|
|
132
134
|
*/
|
|
133
135
|
function lineCount(view) {
|
|
134
|
-
|
|
135
|
-
for (const character of view) if (character === "\n") lines += 1;
|
|
136
|
-
return lines;
|
|
136
|
+
return Iterator.from(view).reduce((lines, character) => lines + Number(character === "\n"), 1);
|
|
137
137
|
}
|
|
138
138
|
/**
|
|
139
139
|
* Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`),
|
|
@@ -171,7 +171,7 @@ function redrawPrefix(previousLines) {
|
|
|
171
171
|
/**
|
|
172
172
|
* Projects any field the walk reads as a line of text into the {@link TextField} the text reducer
|
|
173
173
|
* takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`,
|
|
174
|
-
* `datetime`, `color`, and one `file` entry. The label carries that control's format cue from
|
|
174
|
+
* `datetime`, `color`, one `list` entry, and one `file` entry. The label carries that control's format cue from
|
|
175
175
|
* {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
|
|
176
176
|
* projection carries no rule, because the authoritative form still evaluates the answer this line
|
|
177
177
|
* binds; it exists only so one reducer covers every one of them.
|
|
@@ -300,8 +300,40 @@ function renderUnavailableLine(styler, theme, choices) {
|
|
|
300
300
|
function renderNumberedList(styler, theme, choices) {
|
|
301
301
|
return choices.map((choice, index) => ` ${styler.render(theme.roles.muted, `${String(index + 1)})`)} ${styler.render(theme.roles.content, choice.label)}`).join("\n");
|
|
302
302
|
}
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
// src/server/TerminalRead.ts
|
|
304
|
+
/** Holds reducer state and the rendered line count for one terminal read. */
|
|
305
|
+
var TerminalRead = class {
|
|
306
|
+
#state;
|
|
307
|
+
#lines;
|
|
308
|
+
constructor(state, lines) {
|
|
309
|
+
this.#state = state;
|
|
310
|
+
this.#lines = lines;
|
|
311
|
+
}
|
|
312
|
+
get state() {
|
|
313
|
+
return this.#state;
|
|
314
|
+
}
|
|
315
|
+
get lines() {
|
|
316
|
+
return this.#lines;
|
|
317
|
+
}
|
|
318
|
+
advance(state) {
|
|
319
|
+
this.#state = state;
|
|
320
|
+
}
|
|
321
|
+
paint(lines) {
|
|
322
|
+
this.#lines = lines;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
// src/server/TerminalWalk.ts
|
|
326
|
+
/** Tracks the group whose heading one form walk has displayed. */
|
|
327
|
+
var TerminalWalk = class {
|
|
328
|
+
#group;
|
|
329
|
+
get group() {
|
|
330
|
+
return this.#group;
|
|
331
|
+
}
|
|
332
|
+
enter(group) {
|
|
333
|
+
this.#group = group;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
// src/server/Terminal.ts
|
|
305
337
|
/**
|
|
306
338
|
* Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive
|
|
307
339
|
* form driver, and the only impure part of the terminal stack. {@link ask} walks one form's fields
|
|
@@ -314,11 +346,12 @@ function renderNumberedList(styler, theme, choices) {
|
|
|
314
346
|
* @remarks
|
|
315
347
|
* See {@link TerminalInterface} for the driving contract. The walk itself:
|
|
316
348
|
*
|
|
317
|
-
* - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`,
|
|
318
|
-
* each `file` entry are read as one line of text through {@link fieldToText}, which
|
|
319
|
-
* control's format cue to the label
|
|
320
|
-
*
|
|
321
|
-
* `
|
|
349
|
+
* - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, each `list`
|
|
350
|
+
* entry, and each `file` entry are read as one line of text through {@link fieldToText}, which
|
|
351
|
+
* appends that control's format cue to the label; a `list` reads one entry per line until a blank
|
|
352
|
+
* one and keeps its declared `default` when none is typed. `password`, `confirm`, `editor`,
|
|
353
|
+
* `select`, and `checkbox` each drive their own reducer. An open `select` is a suggestion list
|
|
354
|
+
* plus a typed line, because `open` means the answer need not come from the list.
|
|
322
355
|
* - **The binding projects through `matchesAnswer`.** Every answer is filled as
|
|
323
356
|
* `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the
|
|
324
357
|
* control's own shape, so a bare return on a field with no default binds as absence and the
|
|
@@ -377,14 +410,14 @@ var Terminal = class {
|
|
|
377
410
|
* ends the walk between fields as well as under an active read.
|
|
378
411
|
*/
|
|
379
412
|
async #walk(form, fields) {
|
|
380
|
-
|
|
413
|
+
const walk = new TerminalWalk();
|
|
381
414
|
for (const field of fields) {
|
|
382
415
|
if (form.status !== "editing") return;
|
|
383
416
|
if (field.hidden === true) continue;
|
|
384
417
|
if (form.disabled.has(field.name)) continue;
|
|
385
|
-
if (field.group !== group) {
|
|
386
|
-
|
|
387
|
-
if (group !== void 0) this.#writeGroup(form, group);
|
|
418
|
+
if (field.group !== walk.group) {
|
|
419
|
+
walk.enter(field.group);
|
|
420
|
+
if (walk.group !== void 0) this.#writeGroup(form, walk.group);
|
|
388
421
|
}
|
|
389
422
|
if (field.locked === true) {
|
|
390
423
|
this.#writeLocked(form, field);
|
|
@@ -412,6 +445,7 @@ var Terminal = class {
|
|
|
412
445
|
if (field.control === "editor") return this.#askEditor(form, field);
|
|
413
446
|
if (field.control === "select") return this.#select(form, field);
|
|
414
447
|
if (field.control === "checkbox") return this.#checkbox(form, field);
|
|
448
|
+
if (field.control === "list") return this.#list(form, field);
|
|
415
449
|
if (field.control === "file") return this.#file(form, field);
|
|
416
450
|
return this.#askText(form, field);
|
|
417
451
|
}
|
|
@@ -536,14 +570,23 @@ var Terminal = class {
|
|
|
536
570
|
*/
|
|
537
571
|
async #file(form, field) {
|
|
538
572
|
if (field.multiple === true) this.#output.write(`${this.#formatHint(FILE_HINT)}${_src_core.NEWLINE}`);
|
|
539
|
-
const paths =
|
|
573
|
+
const paths = await this.#collect(form, field, field.multiple === true);
|
|
574
|
+
return paths.length > 0 ? paths : void 0;
|
|
575
|
+
}
|
|
576
|
+
async #list(form, field) {
|
|
577
|
+
this.#output.write(`${this.#formatHint(LIST_HINT)}${_src_core.NEWLINE}`);
|
|
578
|
+
const entries = await this.#collect(form, field, true);
|
|
579
|
+
return entries.length > 0 ? entries : field.default;
|
|
580
|
+
}
|
|
581
|
+
async #collect(form, field, many) {
|
|
582
|
+
const entries = [];
|
|
540
583
|
for (;;) {
|
|
541
584
|
const entry = await this.#askText(form, field);
|
|
542
585
|
if (entry.length === 0) break;
|
|
543
|
-
|
|
544
|
-
if (
|
|
586
|
+
entries.push(entry);
|
|
587
|
+
if (!many) break;
|
|
545
588
|
}
|
|
546
|
-
return
|
|
589
|
+
return entries;
|
|
547
590
|
}
|
|
548
591
|
/** Names the choices a field shows but refuses, above the list the walk drives. */
|
|
549
592
|
#writeUnavailable(choices) {
|
|
@@ -593,34 +636,34 @@ var Terminal = class {
|
|
|
593
636
|
#drive(form, initial, reduce) {
|
|
594
637
|
return new Promise((resolve, reject) => {
|
|
595
638
|
this.#output.write(CURSOR_HIDE);
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
639
|
+
const firstView = (0, _orkestrel_contract.attempt)(() => {
|
|
640
|
+
const view = reduce(initial, (0, _src_core.parseKey)("")).view;
|
|
641
|
+
this.#output.write(view);
|
|
642
|
+
return view;
|
|
643
|
+
});
|
|
644
|
+
if (!firstView.success) {
|
|
601
645
|
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
602
|
-
reject(error);
|
|
646
|
+
reject(firstView.error);
|
|
603
647
|
return;
|
|
604
648
|
}
|
|
605
649
|
const token = {};
|
|
606
|
-
const handler = this.#createHandler(token, initial, lineCount(firstView), reduce, resolve, reject);
|
|
650
|
+
const handler = this.#createHandler(token, initial, lineCount(firstView.value), reduce, resolve, reject);
|
|
607
651
|
this.#enterRaw(token, handler);
|
|
608
652
|
form.answer.catch((error) => this.#interrupt(token, reject, error));
|
|
609
653
|
});
|
|
610
654
|
}
|
|
611
655
|
#createHandler(token, initial, initialLines, reduce, resolve, reject) {
|
|
612
|
-
|
|
613
|
-
let lines = initialLines;
|
|
656
|
+
const read = new TerminalRead(initial, initialLines);
|
|
614
657
|
return (chunk) => {
|
|
615
658
|
try {
|
|
616
|
-
const step = reduce(state, (0, _src_core.parseKey)(chunk));
|
|
617
|
-
|
|
659
|
+
const step = reduce(read.state, (0, _src_core.parseKey)(chunk));
|
|
660
|
+
read.advance(step.state);
|
|
618
661
|
if (step.status === "active") {
|
|
619
|
-
this.#render(step.view, lines);
|
|
620
|
-
|
|
662
|
+
this.#render(step.view, read.lines);
|
|
663
|
+
read.paint(lineCount(step.view));
|
|
621
664
|
return;
|
|
622
665
|
}
|
|
623
|
-
this.#render(step.view, lines);
|
|
666
|
+
this.#render(step.view, read.lines);
|
|
624
667
|
this.#leaveRaw(token);
|
|
625
668
|
this.#output.write(`${CURSOR_SHOW}${_src_core.NEWLINE}`);
|
|
626
669
|
if (step.status === "cancel") reject(new _src_core.TerminalError("CANCEL", "Prompt cancelled"));
|
|
@@ -750,8 +793,7 @@ var Terminal = class {
|
|
|
750
793
|
};
|
|
751
794
|
}
|
|
752
795
|
};
|
|
753
|
-
|
|
754
|
-
//#region src/server/factories.ts
|
|
796
|
+
// src/server/factories.ts
|
|
755
797
|
/**
|
|
756
798
|
* Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,
|
|
757
799
|
* beside the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the
|
|
@@ -816,7 +858,6 @@ var Terminal = class {
|
|
|
816
858
|
function createTerminal(options) {
|
|
817
859
|
return new Terminal(options);
|
|
818
860
|
}
|
|
819
|
-
//#endregion
|
|
820
861
|
exports.CLEAR_DOWN = CLEAR_DOWN;
|
|
821
862
|
exports.CONTROL_HINTS = CONTROL_HINTS;
|
|
822
863
|
exports.CSI_UP = CSI_UP;
|
|
@@ -827,6 +868,7 @@ exports.FALLBACK_CONFIRM_HINT = FALLBACK_CONFIRM_HINT;
|
|
|
827
868
|
exports.FALLBACK_EDITOR_HINT = FALLBACK_EDITOR_HINT;
|
|
828
869
|
exports.FALLBACK_SELECT_HINT = FALLBACK_SELECT_HINT;
|
|
829
870
|
exports.FILE_HINT = FILE_HINT;
|
|
871
|
+
exports.LIST_HINT = LIST_HINT;
|
|
830
872
|
exports.LOCKED_MARK = LOCKED_MARK;
|
|
831
873
|
exports.REFUSAL_MESSAGE = REFUSAL_MESSAGE;
|
|
832
874
|
exports.SUGGESTION_LEAD = SUGGESTION_LEAD;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["CSI","isObject","isFunction","RETURN","isNumber","isString","isBoolean","stdin","isStreamTarget","stdout","createStyler","createPromptTheme","NEWLINE","sanitizeDisplayText","parseValue","matchesAnswer","renderErrorLine","createInputState","reduceInput","createPasswordState","reducePassword","renderPromptHeader","createConfirmState","reduceConfirm","renderHintedHeader","createEditorState","reduceEditor","createSelectState","reduceSelect","createCheckboxState","reduceCheckbox","parseKey","TerminalError","createInterface"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/Terminal.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-terminals constants — the cursor and line-clear ANSI sequences the interactive `Terminal`\n// driver writes to redraw a field view IN PLACE, plus the copy the walk shows around those views:\n// the per-control format hints, the readline-fallback prompts, and the marks for a locked field and\n// an unavailable choice. UPPER_SNAKE, every member exported. Every sequence is built from the\n// console module's own `CSI` prefix, so no raw control character appears in source and this module\n// declares no second copy of that primitive.\n\nimport type { FieldControl } from '@orkestrel/form'\nimport { CSI } from '@orkestrel/console'\n\n/**\n * Holds the cursor-up sequence template (`ESC[{count}A`) —\n * {@link import('./helpers.js').renderCursorUp} interpolates the `{count}` placeholder with the\n * number of lines to climb. Kept as a template so the count stays out of the constant.\n */\nexport const CSI_UP = `${CSI}{count}A`\n\n/**\n * Hides the cursor (`ESC[?25l`) — written before the driver starts redrawing a prompt so the cursor\n * does not flicker across the view during an in-place re-render; paired with {@link CURSOR_SHOW}.\n */\nexport const CURSOR_HIDE = `${CSI}?25l`\n\n/** Shows the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */\nexport const CURSOR_SHOW = `${CSI}?25h`\n\n/**\n * Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the whole previous view,\n * which a `select` or `checkbox` can spread over several lines, in one write before the new view is\n * rendered, so a redraw never leaves orphaned rows behind.\n */\nexport const CLEAR_DOWN = `${CSI}J`\n\n/**\n * Holds the numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback\n * appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are\n * printed numbered and the user types one number on a single readline line.\n */\nexport const FALLBACK_SELECT_HINT = 'Enter a number'\n\n/** Holds the comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */\nexport const FALLBACK_CHECKBOX_HINT = 'Enter numbers separated by commas'\n\n/** Holds the hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */\nexport const FALLBACK_EDITOR_HINT = '(end of input finishes)'\n\n/** Holds the hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */\nexport const FALLBACK_CONFIRM_HINT = '(y/n)'\n\n/**\n * Holds the format cue appended to a field's label for each control the walk reads as a line of text —\n * the terminal has no date picker, no color well, and no file chooser, so the accepted shape is\n * stated instead. A control with no entry needs none: `text` and `editor` accept any line,\n * `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by\n * format. The form's own rules still decide whether the typed value is acceptable.\n */\nexport const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string>>> = Object.freeze({\n\tnumber: '(number)',\n\tdate: '(YYYY-MM-DD)',\n\ttime: '(HH:MM)',\n\tdatetime: '(YYYY-MM-DDTHH:MM)',\n\tcolor: '(#rrggbb)',\n\tfile: '(path)',\n})\n\n/** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */\nexport const FILE_HINT = 'One path per line, blank to finish'\n\n/**\n * Holds the lead on the line listing an open `select`'s offered values, which a typed answer can\n * ignore.\n */\nexport const SUGGESTION_LEAD = 'Suggestions'\n\n/**\n * Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a\n * reader sees why one is missing from the list it heads.\n */\nexport const UNAVAILABLE_LEAD = 'Unavailable'\n\n/** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */\nexport const LOCKED_MARK = '(locked)'\n\n/**\n * States what a field is told when the walk read an answer the control cannot hold — a word typed into a\n * `number`, an off-list value typed into an open `select` whose choice is refused. The value binds\n * as absence and this message is invalidated onto the field, so the walk re-asks it with the reason\n * on screen.\n */\nexport const REFUSAL_MESSAGE = 'Enter a value this field accepts'\n","// Pure helpers for the server terminal driver — every function here is exported and unit-tested.\n// The families here are all total: the stream-boundary guards that narrow `process.stdin` / any\n// injected stream without an assertion; the cursor math the driver uses to redraw a field view IN\n// PLACE (count a view's lines, build the cursor-up sequence, assemble the reposition-and-clear\n// prefix); and the projections and line renderers the whole-form walk needs that no reducer owns —\n// a control read as text, a value shown read-only, the choices a field actually offers, and the\n// group, locked, suggestion, unavailable, and numbered-list lines. The impure driver only feeds\n// bytes into the reducers and writes the strings these helpers build. The output boundary is\n// narrowed by the console module's own `isStreamTarget`, which this module does not redeclare.\n\nimport type { FieldChoice, FieldValue, FormField, TextField } from '@orkestrel/form'\nimport type { PromptTheme } from '@src/core'\nimport type { StylerInterface } from '@orkestrel/console'\nimport type { InputStreamInterface } from './types.js'\nimport { RETURN } from '@src/core'\nimport {\n\tCLEAR_DOWN,\n\tCONTROL_HINTS,\n\tCSI_UP,\n\tLOCKED_MARK,\n\tSUGGESTION_LEAD,\n\tUNAVAILABLE_LEAD,\n} from './constants.js'\nimport { isBoolean, isFunction, isNumber, isObject, isString } from '@orkestrel/contract'\n\n/**\n * Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /\n * `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for\n * anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,\n * or a fake TTY a test injects) to the exact slice the driver reads, never an assertion.\n *\n * @remarks\n * Only `on` / `off` are required (the irreducible event seam); `setRawMode` / `resume` / `pause` /\n * `isTTY` are optional on {@link InputStreamInterface}, so their absence does not disqualify a stream\n * — a piped, non-TTY stream is still a valid input, one the driver reads through the readline\n * fallback rather than raw mode.\n *\n * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)\n * @returns True if `value` has callable `on` and `off`; false otherwise\n */\nexport function isInputStream(value: unknown): value is InputStreamInterface {\n\treturn (\n\t\tisObject(value) &&\n\t\t'on' in value &&\n\t\tisFunction(value.on) &&\n\t\t'off' in value &&\n\t\tisFunction(value.off)\n\t)\n}\n\n/**\n * Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard\n * checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface`\n * requires as its `input`. The non-TTY fallback narrows the resolved input stream through this\n * before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a\n * `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns\n * `false` for a minimal fake that isn't a full readable.\n *\n * @param value - The resolved input stream (or any value crossing the boundary)\n * @returns True if `value` has the readable methods readline needs; false otherwise\n */\nexport function isReadable(value: unknown): value is NodeJS.ReadableStream {\n\treturn (\n\t\tisObject(value) &&\n\t\t'read' in value &&\n\t\tisFunction(value.read) &&\n\t\t'pipe' in value &&\n\t\tisFunction(value.pipe)\n\t)\n}\n\n/**\n * Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and\n * exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to\n * choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a\n * live re-render; false selects the `node:readline` line-input fallback, because a piped or\n * non-terminal stream cannot enter raw mode. Total — never throws.\n *\n * @param input - The resolved {@link InputStreamInterface}\n * @returns True if the stream is a TTY with `setRawMode`; false otherwise\n */\nexport function supportsRawMode(input: InputStreamInterface): boolean {\n\treturn input.isTTY === true && isFunction(input.setRawMode)\n}\n\n/**\n * Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so\n * a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of\n * the in-place re-render: the driver records the line count of the view it wrote so the next redraw\n * knows how far up to move the cursor before overwriting. Total; an empty string is one empty line.\n *\n * @param view - The rendered (possibly multi-line, possibly ANSI-styled) view string\n * @returns The number of lines the view spans (always at least 1)\n */\nexport function lineCount(view: string): number {\n\tlet lines = 1\n\tfor (const character of view) {\n\t\tif (character === '\\n') lines += 1\n\t}\n\treturn lines\n}\n\n/**\n * Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`),\n * or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A`\n * is a wasted write. The pure step the in-place re-render uses to climb back over the previous view\n * before clearing it. Total.\n *\n * @param count - How many lines to move the cursor up\n * @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`\n */\nexport function renderCursorUp(count: number): string {\n\tif (count <= 0) return ''\n\treturn `${CSI_UP.replace('{count}', String(count))}`\n}\n\n/**\n * Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place\n * — given the line count of the previous view, it moves the cursor up over those lines, returns it\n * to column 0, and erases everything from there to the end of the screen, so the next view is drawn\n * on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes\n * this immediately followed by the new view.\n *\n * @remarks\n * For the first render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so\n * the prefix is a carriage return + clear-down — the prompt draws from the current line. For a\n * subsequent render it climbs `previousLines - 1` lines (the cursor is on the last line of the prior\n * view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable\n * without a real terminal.\n *\n * @param previousLines - The line count of the view on screen (from {@link lineCount})\n * @returns The control-sequence prefix to write before the new view\n */\nexport function redrawPrefix(previousLines: number): string {\n\treturn `${renderCursorUp(previousLines - 1)}${RETURN}${CLEAR_DOWN}`\n}\n\n/**\n * Projects any field the walk reads as a line of text into the {@link TextField} the text reducer\n * takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`,\n * `datetime`, `color`, and one `file` entry. The label carries that control's format cue from\n * {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The\n * projection carries no rule, because the authoritative form still evaluates the answer this line\n * binds; it exists only so one reducer covers every one of them.\n *\n * @param field - The field being read\n * @returns The text field the reducer renders for it\n *\n * @example\n * ```ts\n * fieldToText({ control: 'date', name: 'born', label: 'Birthday' })\n * // { control: 'text', name: 'born', label: 'Birthday (YYYY-MM-DD)' }\n * ```\n */\nexport function fieldToText(field: FormField): TextField {\n\tconst label = field.label ?? field.name\n\tconst hint = CONTROL_HINTS[field.control]\n\tconst seed = 'default' in field ? field.default : undefined\n\tconst text = isNumber(seed) ? String(seed) : seed\n\treturn {\n\t\tcontrol: 'text',\n\t\tname: field.name,\n\t\tlabel: hint === undefined ? label : `${label} ${hint}`,\n\t\t...(isString(text) ? { default: text } : {}),\n\t}\n}\n\n/**\n * Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as\n * `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders\n * as nothing, because a locked field nobody has answered has nothing to show.\n *\n * @param value - The answer the form holds for a field, or absence\n * @returns The text to render for it\n */\nexport function valueToText(value: FieldValue | undefined): string {\n\tif (value === undefined) return ''\n\tif (isString(value)) return value\n\tif (isNumber(value)) return String(value)\n\tif (isBoolean(value)) return value ? 'yes' : 'no'\n\treturn value.join(', ')\n}\n\n/**\n * Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled\n * choice's value at every door, including a fill, so the walk never puts one in front of the\n * cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.\n *\n * @param choices - The field's declared choices\n * @returns The choices the walk offers, in declared order\n */\nexport function filterEnabled(choices: readonly FieldChoice[]): readonly FieldChoice[] {\n\treturn choices.filter((choice) => choice.disabled !== true)\n}\n\n/**\n * Returns the choices a `select` or `checkbox` field shows but refuses — the complement of\n * {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees\n * why a declared choice is missing from it.\n *\n * @param choices - The field's declared choices\n * @returns The refused choices, in declared order\n */\nexport function filterDisabled(choices: readonly FieldChoice[]): readonly FieldChoice[] {\n\treturn choices.filter((choice) => choice.disabled === true)\n}\n\n/**\n * Renders the section header the walk writes when it enters a new field group, painted by the\n * `message` role.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param label - The group's declared label, falling back to its own name\n * @returns The rendered section header\n */\nexport function renderGroupHeader(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tlabel: string,\n): string {\n\treturn styler.render(theme.roles.message, label)\n}\n\n/**\n * Renders the read-only line a locked field shows — its label, the {@link LOCKED_MARK}, and the\n * answer the form already holds. The walk writes this instead of a prompt, because the field is\n * still validated and still submitted but must not be edited here.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param label - The field's label\n * @param value - The held answer, from {@link valueToText}\n * @returns The rendered line, with no trailing space when there is nothing to show\n */\nexport function renderLockedLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tlabel: string,\n\tvalue: string,\n): string {\n\tconst head = `${styler.render(theme.roles.muted, theme.icons.dot)} ${styler.render(theme.roles.message, label)} ${styler.render(theme.roles.hint, LOCKED_MARK)}`\n\treturn value.length === 0 ? head : `${head} ${styler.render(theme.roles.content, value)}`\n}\n\n/**\n * Renders the line listing an open select's offered values above its text prompt — a suggestion\n * list, because an open select admits an answer the list does not offer.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The choices the open select offers, from {@link filterEnabled}\n * @returns The rendered suggestion line\n */\nexport function renderSuggestionLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\tconst values = choices.map((choice) => choice.value).join(', ')\n\treturn styler.render(theme.roles.hint, `${SUGGESTION_LEAD}: ${values}`)\n}\n\n/**\n * Renders the line naming the choices a field shows but refuses, written above the list the walk\n * drives.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The refused choices, from {@link filterDisabled}\n * @returns The rendered unavailable line\n */\nexport function renderUnavailableLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\tconst labels = choices.map((choice) => choice.label).join(', ')\n\treturn styler.render(theme.roles.muted, `${UNAVAILABLE_LEAD}: ${labels}`)\n}\n\n/**\n * Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate\n * with arrow keys, so each offered choice is printed with the number the reader types back. One\n * line per choice, with no trailing newline.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The choices the walk offers, from {@link filterEnabled}\n * @returns The rendered list\n */\nexport function renderNumberedList(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\treturn choices\n\t\t.map(\n\t\t\t(choice, index) =>\n\t\t\t\t` ${styler.render(theme.roles.muted, `${String(index + 1)})`)} ${styler.render(theme.roles.content, choice.label)}`,\n\t\t)\n\t\t.join('\\n')\n}\n","import type {\n\tFieldChoice,\n\tFieldError,\n\tFieldValue,\n\tFormField,\n\tFormInterface,\n\tFormValues,\n} from '@orkestrel/form'\nimport type {\n\tCheckboxField,\n\tConfirmField,\n\tEditorField,\n\tFileField,\n\tPasswordField,\n\tSelectField,\n} from '@orkestrel/form'\nimport type { KeyEvent, PromptStep, PromptTheme, TerminalInterface } from '@src/core'\nimport type { StylerInterface } from '@orkestrel/console'\nimport type { StreamTargetInterface } from '@orkestrel/console/server'\nimport type { Interface } from 'node:readline'\nimport type { InputStreamInterface, TerminalOptions } from './types.js'\nimport {\n\tcreateCheckboxState,\n\tcreateConfirmState,\n\tcreateEditorState,\n\tcreateInputState,\n\tcreatePasswordState,\n\tcreatePromptTheme,\n\tcreateSelectState,\n\tNEWLINE,\n\tparseKey,\n\treduceCheckbox,\n\treduceConfirm,\n\treduceEditor,\n\treduceInput,\n\treducePassword,\n\treduceSelect,\n\trenderErrorLine,\n\trenderHintedHeader,\n\trenderPromptHeader,\n\tsanitizeDisplayText,\n\tTerminalError,\n} from '@src/core'\nimport { matchesAnswer, parseValue } from '@orkestrel/form'\nimport { createStyler } from '@orkestrel/console'\nimport { isStreamTarget } from '@orkestrel/console/server'\nimport { createInterface } from 'node:readline'\nimport { stdin, stdout } from 'node:process'\nimport {\n\tCURSOR_HIDE,\n\tCURSOR_SHOW,\n\tFALLBACK_CHECKBOX_HINT,\n\tFALLBACK_CONFIRM_HINT,\n\tFALLBACK_EDITOR_HINT,\n\tFALLBACK_SELECT_HINT,\n\tFILE_HINT,\n\tREFUSAL_MESSAGE,\n} from './constants.js'\nimport {\n\tfieldToText,\n\tfilterDisabled,\n\tfilterEnabled,\n\tisInputStream,\n\tisReadable,\n\tlineCount,\n\tredrawPrefix,\n\trenderGroupHeader,\n\trenderLockedLine,\n\trenderNumberedList,\n\trenderSuggestionLine,\n\trenderUnavailableLine,\n\tsupportsRawMode,\n\tvalueToText,\n} from './helpers.js'\n\n/**\n * Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive\n * form driver, and the only impure part of the terminal stack. {@link ask} walks one form's fields\n * in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching pure reducer,\n * renders each returned view in place, binds every answer through the form's own `fill`, and\n * re-asks what the form refused. It owns no form logic: the schema, the rules, the values, and the\n * settlement all belong to the form it is given, and this class owns only raw mode, the cursor, and\n * the re-render.\n *\n * @remarks\n * See {@link TerminalInterface} for the driving contract. The walk itself:\n *\n * - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, and\n * each `file` entry are read as one line of text through {@link fieldToText}, which appends that\n * control's format cue to the label. `password`, `confirm`, `editor`, `select`, and `checkbox`\n * each drive their own reducer. An open `select` is a suggestion list plus a typed line, because\n * `open` means the answer need not come from the list.\n * - **The binding projects through `matchesAnswer`.** Every answer is filled as\n * `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the\n * control's own shape, so a bare return on a field with no default binds as absence and the\n * form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and\n * invalidates the field, so the walk asks again with the reason on screen.\n * - **Visibility is honored.** A `hidden` field and a field in `form.disabled` are skipped; a\n * `locked` field renders read-only; entering a new group writes its label as a section header.\n * - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring\n * fields the walk can edit and submits again. When every erroring field is one the walk cannot\n * edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could\n * not change the answer.\n * - **Raw-mode leak-free.** Raw mode is entered once per field and always cleaned up: on submit, on\n * cancel, on a throw, and when the form is abandoned under an active read.\n * - **Non-TTY fallback.** When `input` is not a TTY, the same walk runs over `node:readline` line\n * input: `select` and `checkbox` print a numbered list, and `editor` reads to end of input.\n */\nexport class Terminal implements TerminalInterface {\n\treadonly #input: InputStreamInterface\n\treadonly #output: StreamTargetInterface\n\treadonly #styler: StylerInterface\n\treadonly #theme: PromptTheme\n\treadonly #handlers = new Map<object, (chunk: string | Uint8Array) => void>()\n\treadonly #queue: string[] = []\n\t#interface: Interface | undefined\n\t#taker: ((line: string | undefined) => void) | undefined\n\t#ended = false\n\n\tconstructor(options?: TerminalOptions) {\n\t\t// Resolve each stream through its guard: a present, well-shaped injected stream is used as is;\n\t\t// otherwise the real process stream — no assertion, and an absent option falls through.\n\t\tthis.#input = isInputStream(options?.input) ? options.input : stdin\n\t\tthis.#output = isStreamTarget(options?.output) ? options.output : stdout\n\t\tthis.#styler = createStyler()\n\t\tthis.#theme = createPromptTheme(options?.theme)\n\t}\n\n\t// === The contract\n\n\tasync ask(form: FormInterface): Promise<FormValues> {\n\t\ttry {\n\t\t\tthis.#report(form, form.errors)\n\t\t\tawait this.#walk(form, form.schema.fields)\n\t\t\tfor (;;) {\n\t\t\t\tif (form.status !== 'editing') return await form.answer\n\t\t\t\tconst result = form.submit()\n\t\t\t\tif (result.success) return await form.answer\n\t\t\t\tthis.#report(form, result.error)\n\t\t\t\tconst again = this.#collectEditable(form, result.error)\n\t\t\t\t// Nothing the walk can edit, or an input stream that has already ended: asking again would\n\t\t\t\t// render the same fields and read the same answers forever, so abandon the form instead.\n\t\t\t\t// The failures are on screen either way, so the reader sees what could not be answered.\n\t\t\t\tif (again.length === 0 || this.#ended) {\n\t\t\t\t\tform.destroy()\n\t\t\t\t\treturn await form.answer\n\t\t\t\t}\n\t\t\t\tawait this.#walk(form, again)\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#close()\n\t\t}\n\t}\n\n\t// === The walk\n\n\t/**\n\t * Asks `fields` in the order given, skipping what the walk must not touch and rendering what it\n\t * must not edit. Returns early the moment the form stops being editable, so an abandoned form\n\t * ends the walk between fields as well as under an active read.\n\t */\n\tasync #walk(form: FormInterface, fields: readonly FormField[]): Promise<void> {\n\t\tlet group: string | undefined\n\t\tfor (const field of fields) {\n\t\t\tif (form.status !== 'editing') return\n\t\t\tif (field.hidden === true) continue\n\t\t\tif (form.disabled.has(field.name)) continue\n\t\t\tif (field.group !== group) {\n\t\t\t\tgroup = field.group\n\t\t\t\tif (group !== undefined) this.#writeGroup(form, group)\n\t\t\t}\n\t\t\tif (field.locked === true) {\n\t\t\t\tthis.#writeLocked(form, field)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst raw = await this.#read(form, field)\n\t\t\tif (form.status !== 'editing') return\n\t\t\tthis.#bind(form, field, raw)\n\t\t}\n\t}\n\n\t/** Writes a group's label as a section header, resolving the schema's declared label and falling back to the group's own name. */\n\t#writeGroup(form: FormInterface, name: string): void {\n\t\tconst label = form.schema.groups?.find((group) => group.name === name)?.label ?? name\n\t\tthis.#output.write(\n\t\t\t`${NEWLINE}${renderGroupHeader(this.#styler, this.#theme, sanitizeDisplayText(label))}${NEWLINE}`,\n\t\t)\n\t}\n\n\t/** Renders a locked field read-only — its label, its mark, and the answer the form already holds. */\n\t#writeLocked(form: FormInterface, field: FormField): void {\n\t\tconst line = renderLockedLine(\n\t\t\tthis.#styler,\n\t\t\tthis.#theme,\n\t\t\tsanitizeDisplayText(field.label ?? field.name),\n\t\t\tsanitizeDisplayText(valueToText(form.values[field.name])),\n\t\t)\n\t\tthis.#output.write(`${line}${NEWLINE}`)\n\t}\n\n\t/** Reads one field through the reducer its control names, resolving the raw answer the binding projects. */\n\t#read(form: FormInterface, field: FormField): Promise<FieldValue | undefined> {\n\t\tif (field.control === 'password') return this.#askPassword(form, field)\n\t\tif (field.control === 'confirm') return this.#confirm(form, field)\n\t\tif (field.control === 'editor') return this.#askEditor(form, field)\n\t\tif (field.control === 'select') return this.#select(form, field)\n\t\tif (field.control === 'checkbox') return this.#checkbox(form, field)\n\t\tif (field.control === 'file') return this.#file(form, field)\n\t\treturn this.#askText(form, field)\n\t}\n\n\t/**\n\t * Binds one raw answer to the form — the single binding, and the only place this driver writes a\n\t * value. `parseValue` coerces the raw answer to the control's own shape and `matchesAnswer`\n\t * projects a blank one to absence, which is what keeps `required` refusing a bare return. A raw\n\t * answer the control cannot hold binds as absence and invalidates the field, so the field comes\n\t * back on the next pass carrying the reason rather than vanishing silently.\n\t */\n\t#bind(form: FormInterface, field: FormField, raw: FieldValue | undefined): void {\n\t\tform.touch(field.name)\n\t\tconst value = raw === undefined ? undefined : parseValue(field, raw)\n\t\tform.fill(field.name, matchesAnswer(value) ? value : undefined)\n\t\tif (value === undefined && matchesAnswer(raw)) form.invalidate(field.name, REFUSAL_MESSAGE)\n\t}\n\n\t/** 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. */\n\t#collectEditable(form: FormInterface, errors: readonly FieldError[]): readonly FormField[] {\n\t\tconst fields: FormField[] = []\n\t\tfor (const error of errors) {\n\t\t\tconst field = form.field(error.field)\n\t\t\tif (field === undefined) continue\n\t\t\tif (field.hidden === true || field.locked === true) continue\n\t\t\tif (form.disabled.has(field.name)) continue\n\t\t\tif (fields.some((held) => held.name === field.name)) continue\n\t\t\tfields.push(field)\n\t\t}\n\t\treturn fields\n\t}\n\n\t/** Writes every supplied failure against its field's label. */\n\t#report(form: FormInterface, errors: readonly FieldError[]): void {\n\t\tfor (const error of errors) {\n\t\t\tconst label = sanitizeDisplayText(form.field(error.field)?.label ?? error.field)\n\t\t\tconst message = sanitizeDisplayText(error.message)\n\t\t\tconst line = renderErrorLine(this.#styler, this.#theme, `${label}: ${message}`)\n\t\t\tthis.#output.write(`${line}${NEWLINE}`)\n\t\t}\n\t}\n\n\t// === The controls\n\n\t/** Reads a field as one line of text — `text` itself and the controls a terminal has no widget for. */\n\t#askText(form: FormInterface, field: FormField): Promise<string> {\n\t\tconst state = createInputState(fieldToText(field), this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceInput)\n\t\treturn this.#line(form, state.message, state.default)\n\t}\n\n\t/** Reads a secret — masked live in raw mode, and read without echo through readline on a stream that cannot enter it. */\n\t#askPassword(form: FormInterface, field: PasswordField): Promise<string> {\n\t\tconst state = createPasswordState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reducePassword)\n\t\treturn this.#prompt(form, renderPromptHeader(this.#styler, this.#theme, state.message))\n\t}\n\n\t/**\n\t * Reads a yes or no. The fallback accepts `y` / `yes` / `n` / `no` in any case and takes the\n\t * field's default for a bare line; anything else is returned as typed, so the binding refuses it\n\t * and the walk asks again rather than silently reading it as no.\n\t */\n\tasync #confirm(form: FormInterface, field: ConfirmField): Promise<FieldValue> {\n\t\tconst state = createConfirmState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceConfirm)\n\t\tconst header = renderHintedHeader(\n\t\t\tthis.#styler,\n\t\t\tthis.#theme,\n\t\t\tstate.message,\n\t\t\tFALLBACK_CONFIRM_HINT,\n\t\t)\n\t\tconst line = await this.#prompt(form, header)\n\t\tconst answer = line.trim().toLowerCase()\n\t\tif (answer.length === 0) return state.default\n\t\tif (answer === 'y' || answer === 'yes') return true\n\t\tif (answer === 'n' || answer === 'no') return false\n\t\treturn line.trim()\n\t}\n\n\t/** Reads text over many lines — ctrl-d finishes in raw mode, and end of input finishes on a piped stream. */\n\t#askEditor(form: FormInterface, field: EditorField): Promise<string> {\n\t\tconst state = createEditorState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceEditor)\n\t\treturn this.#block(form, state.message, state.default)\n\t}\n\n\t/**\n\t * Reads one choice. A disabled choice is named above the list and never offered, because the form\n\t * refuses its value at every door. An open select is a suggestion list plus a typed line, since\n\t * `open` admits an answer the list does not offer; a closed select with nothing left to offer\n\t * resolves absence and lets the form's own rules report it.\n\t */\n\tasync #select(form: FormInterface, field: SelectField): Promise<FieldValue | undefined> {\n\t\tconst choices = filterEnabled(field.choices)\n\t\tthis.#writeUnavailable(field.choices)\n\t\tif (field.open === true) {\n\t\t\tif (choices.length > 0) {\n\t\t\t\tconst display = choices.map((choice) => ({\n\t\t\t\t\t...choice,\n\t\t\t\t\tvalue: sanitizeDisplayText(choice.value),\n\t\t\t\t}))\n\t\t\t\tthis.#output.write(`${renderSuggestionLine(this.#styler, this.#theme, display)}${NEWLINE}`)\n\t\t\t}\n\t\t\treturn this.#askText(form, field)\n\t\t}\n\t\tif (choices.length === 0) return undefined\n\t\tconst state = createSelectState({ ...field, choices }, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceSelect)\n\t\tthis.#writeList(state.message, choices)\n\t\tconst line = await this.#prompt(form, this.#formatHint(`${FALLBACK_SELECT_HINT}:`))\n\t\treturn choices[Number.parseInt(line.trim(), 10) - 1]?.value\n\t}\n\n\t/**\n\t * Reads any number of choices. Disabled choices are named above the list and never offered, so a\n\t * box the form would refuse can never be ticked. An empty answer is an answered \"none of them\",\n\t * which is what `matchesAnswer` says an empty list means.\n\t */\n\tasync #checkbox(form: FormInterface, field: CheckboxField): Promise<FieldValue> {\n\t\tconst choices = filterEnabled(field.choices)\n\t\tthis.#writeUnavailable(field.choices)\n\t\tconst state = createCheckboxState({ ...field, choices }, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceCheckbox)\n\t\tthis.#writeList(state.message, choices)\n\t\tconst line = await this.#prompt(form, this.#formatHint(`${FALLBACK_CHECKBOX_HINT}:`))\n\t\treturn line\n\t\t\t.split(',')\n\t\t\t.map((part) => choices[Number.parseInt(part.trim(), 10) - 1]?.value)\n\t\t\t.filter((value): value is string => value !== undefined)\n\t}\n\n\t/**\n\t * Reads file paths — names only, because bytes never enter a form. A `multiple` field collects one\n\t * path per line until a blank one, and a single field takes one. No path at all is absence rather\n\t * than an empty list, because the reader answered a blank line.\n\t */\n\tasync #file(form: FormInterface, field: FileField): Promise<FieldValue | undefined> {\n\t\tif (field.multiple === true) this.#output.write(`${this.#formatHint(FILE_HINT)}${NEWLINE}`)\n\t\tconst paths: string[] = []\n\t\tfor (;;) {\n\t\t\tconst entry = await this.#askText(form, field)\n\t\t\tif (entry.length === 0) break\n\t\t\tpaths.push(entry)\n\t\t\tif (field.multiple !== true || this.#ended) break\n\t\t}\n\t\treturn paths.length > 0 ? paths : undefined\n\t}\n\n\t/** Names the choices a field shows but refuses, above the list the walk drives. */\n\t#writeUnavailable(choices: readonly FieldChoice[]): void {\n\t\tconst refused = filterDisabled(choices)\n\t\tif (refused.length === 0) return\n\t\tthis.#output.write(`${renderUnavailableLine(this.#styler, this.#theme, refused)}${NEWLINE}`)\n\t}\n\n\t/** Writes a field's header above its numbered choice list — the non-TTY presentation of a choice field. */\n\t#writeList(message: string, choices: readonly FieldChoice[]): void {\n\t\tconst header = renderPromptHeader(this.#styler, this.#theme, message)\n\t\tconst list = renderNumberedList(this.#styler, this.#theme, choices)\n\t\tthis.#output.write(`${header}${NEWLINE}${list}${NEWLINE}`)\n\t}\n\n\t/** Paints one line of supplementary instruction through the `hint` role. */\n\t#formatHint(text: string): string {\n\t\treturn this.#styler.render(this.#theme.roles.hint, text)\n\t}\n\n\t// === The raw-mode kernel\n\n\t/**\n\t * Isolates the irreducible Node raw-mode primitive — the ONLY place raw mode is touched. Switches the input\n\t * into raw mode (each keypress delivered immediately, no echo), resumes its flow, and subscribes\n\t * `handler` to `'data'`. The paired {@link #leaveRaw} operation unsubscribes the exact handler,\n\t * leaves raw mode, and pauses the stream on submit, cancel, abandon, or throw.\n\t */\n\t#enterRaw(token: object, handler: (chunk: string | Uint8Array) => void): void {\n\t\tthis.#handlers.set(token, handler)\n\t\tthis.#input.setRawMode?.(true)\n\t\tthis.#input.resume?.()\n\t\tthis.#input.on('data', handler)\n\t}\n\n\t#leaveRaw(token: object): void {\n\t\tconst handler = this.#handlers.get(token)\n\t\tif (handler === undefined) return\n\t\tthis.#handlers.delete(token)\n\t\tthis.#input.off('data', handler)\n\t\tthis.#input.setRawMode?.(false)\n\t\tthis.#input.pause?.()\n\t}\n\n\t/**\n\t * Drives ONE field over raw-mode stdin — the generic engine every interactive control shares.\n\t * Renders the reducer's initial view, enters raw mode once, and on each keypress runs `parseKey` →\n\t * `reduce` → an in-place re-render; on `submit` it cleans up and resolves the reducer's value, on\n\t * `cancel` (ctrl-c) it cleans up and rejects a {@link TerminalError} coded `CANCEL`. Abandoning\n\t * the form ends the read too, through the rejection of its own `answer`. Raw mode is entered\n\t * exactly once and cleaned up on every exit path, so no raw mode and no `'data'` listener ever\n\t * leaks. The cursor is hidden for the duration and restored on exit.\n\t */\n\t#drive<T, S>(\n\t\tform: FormInterface,\n\t\tinitial: S,\n\t\treduce: (state: S, key: KeyEvent) => PromptStep<T, S>,\n\t): Promise<T> {\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\t// Render the first view, tracking how many lines it spans so the next redraw climbs over them.\n\t\t\tthis.#output.write(CURSOR_HIDE)\n\t\t\tlet firstView: string\n\t\t\ttry {\n\t\t\t\tfirstView = reduce(initial, parseKey('')).view\n\t\t\t\tthis.#output.write(firstView)\n\t\t\t} catch (error) {\n\t\t\t\t// Raw mode was never entered yet, but the cursor was hidden — restore it before rejecting.\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\treject(error)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst token = {}\n\t\t\tconst handler = this.#createHandler(\n\t\t\t\ttoken,\n\t\t\t\tinitial,\n\t\t\t\tlineCount(firstView),\n\t\t\t\treduce,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t)\n\t\t\tthis.#enterRaw(token, handler)\n\t\t\t// An abandoned form ends the read: its own `answer` rejects, so release the terminal and fail\n\t\t\t// this field with the form's error. A read that already finished holds no token and is a no-op.\n\t\t\tvoid form.answer.catch((error: unknown) => this.#interrupt(token, reject, error))\n\t\t})\n\t}\n\n\t#createHandler<T, S>(\n\t\ttoken: object,\n\t\tinitial: S,\n\t\tinitialLines: number,\n\t\treduce: (state: S, key: KeyEvent) => PromptStep<T, S>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void,\n\t): (chunk: string | Uint8Array) => void {\n\t\tlet state = initial\n\t\tlet lines = initialLines\n\t\treturn (chunk) => {\n\t\t\ttry {\n\t\t\t\tconst step = reduce(state, parseKey(chunk))\n\t\t\t\tstate = step.state\n\t\t\t\tif (step.status === 'active') {\n\t\t\t\t\tthis.#render(step.view, lines)\n\t\t\t\t\tlines = lineCount(step.view)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Terminal step (submit / cancel): paint the final committed view, then tear down.\n\t\t\t\tthis.#render(step.view, lines)\n\t\t\t\tthis.#leaveRaw(token)\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\tif (step.status === 'cancel') reject(new TerminalError('CANCEL', 'Prompt cancelled'))\n\t\t\t\telse if (step.status === 'submit' && step.value !== undefined) resolve(step.value)\n\t\t\t\telse reject(new TerminalError('DRIVER', 'submit produced no value'))\n\t\t\t} catch (error) {\n\t\t\t\t// A throw inside the reducer/styler: tear down raw mode + the listener, restore the cursor\n\t\t\t\t// exactly as the normal exit path does, then reject.\n\t\t\t\tthis.#leaveRaw(token)\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\treject(error)\n\t\t\t}\n\t\t}\n\t}\n\n\t/** 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. */\n\t#interrupt(token: object, reject: (reason?: unknown) => void, error: unknown): void {\n\t\tif (!this.#handlers.has(token)) return\n\t\tthis.#leaveRaw(token)\n\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\treject(error)\n\t}\n\n\t/** 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}). */\n\t#render(view: string, previousLines: number): void {\n\t\tthis.#output.write(`${redrawPrefix(previousLines)}${view}`)\n\t}\n\n\t// === Non-TTY fallbacks (node:readline line input)\n\n\t/** Reads one line for a text-shaped field, taking the field's default when the line is bare. */\n\tasync #line(form: FormInterface, message: string, seed: string): Promise<string> {\n\t\tconst answer = await this.#prompt(form, renderPromptHeader(this.#styler, this.#theme, message))\n\t\treturn answer.length > 0 ? answer : seed\n\t}\n\n\t/**\n\t * Reads a whole block for an `editor` field — a piped stream has no ctrl-d keypress, so end of\n\t * input is the terminator. It drains every remaining line, which is why nothing after an `editor`\n\t * field can be asked on the same stream.\n\t */\n\tasync #block(form: FormInterface, message: string, seed: string): Promise<string> {\n\t\tconst header = renderHintedHeader(this.#styler, this.#theme, message, FALLBACK_EDITOR_HINT)\n\t\tthis.#output.write(`${header}${NEWLINE}`)\n\t\tconst collected: string[] = []\n\t\tfor (;;) {\n\t\t\tconst line = await this.#next(form)\n\t\t\tif (line === undefined) break\n\t\t\tcollected.push(line)\n\t\t}\n\t\tconst text = collected.join('\\n')\n\t\treturn text.length > 0 ? text : seed\n\t}\n\n\t/** Writes one field's header and reads back the line the reader answers it with, or `''` once the stream has ended. */\n\tasync #prompt(form: FormInterface, header: string): Promise<string> {\n\t\tthis.#output.write(`${header} `)\n\t\tconst line = await this.#next(form)\n\t\treturn line ?? ''\n\t}\n\n\t/**\n\t * Takes the next line the input carries — from the buffer when the reader has already read ahead,\n\t * and otherwise by waiting for one. It resolves ABSENCE once the stream has ended, so a walk over\n\t * an exhausted stream settles instead of waiting forever, and it rejects when the form is\n\t * abandoned, so an unanswerable read ends with the form rather than outliving it.\n\t */\n\t#next(form: FormInterface): Promise<string | undefined> {\n\t\tconst buffered = this.#queue.shift()\n\t\tif (buffered !== undefined) return Promise.resolve(buffered)\n\t\tif (this.#ended) return Promise.resolve(undefined)\n\t\tthis.#startReader()\n\t\treturn new Promise<string | undefined>((resolve, reject) => {\n\t\t\tthis.#taker = resolve\n\t\t\tvoid form.answer.catch((error: unknown) => {\n\t\t\t\tif (this.#taker !== resolve) return\n\t\t\t\tthis.#taker = undefined\n\t\t\t\treject(error)\n\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Opens the walk's ONE readline interface, or keeps the open one. A whole form is many questions\n\t * over one stream, and an interface reads ahead: a fresh interface per question would swallow\n\t * every line it read past the one it was asked for, so the walk holds a single reader and buffers\n\t * what arrives early. It is closed when the walk ends, so the process is free to exit.\n\t */\n\t#startReader(): void {\n\t\tif (this.#interface !== undefined) return\n\t\tconst rl = createInterface(this.#openReadline())\n\t\trl.on('line', (line) => this.#accept(line))\n\t\trl.on('close', () => this.#finish())\n\t\tthis.#interface = rl\n\t}\n\n\t/** Hands a line to whoever is waiting for it, or buffers it for the next question. */\n\t#accept(line: string): void {\n\t\tconst taker = this.#taker\n\t\tif (taker === undefined) {\n\t\t\tthis.#queue.push(line)\n\t\t\treturn\n\t\t}\n\t\tthis.#taker = undefined\n\t\ttaker(line)\n\t}\n\n\t/** Records that no further line can arrive and releases whoever was waiting for one. */\n\t#finish(): void {\n\t\tthis.#ended = true\n\t\tconst taker = this.#taker\n\t\tif (taker === undefined) return\n\t\tthis.#taker = undefined\n\t\ttaker(undefined)\n\t}\n\n\t/** Closes the walk's reader and releases a waiting question — the walk is over, so the stream must stop holding the process open. */\n\t#close(): void {\n\t\tconst taker = this.#taker\n\t\tthis.#taker = undefined\n\t\tif (taker !== undefined) taker(undefined)\n\t\tconst rl = this.#interface\n\t\tif (rl === undefined) return\n\t\tthis.#interface = undefined\n\t\t// Drop the close listener first: this close is the walk ending, not the stream ending, and\n\t\t// recording it as end of input would refuse the next walk its input.\n\t\trl.removeAllListeners('close')\n\t\trl.close()\n\t}\n\n\t/**\n\t * Narrows the resolved input to the `node:readline` `createInterface` boundary, never an assertion.\n\t * The fallback only runs on a real piped `process.stdin` (or a `PassThrough` a test injects), both\n\t * genuine readables; a minimal non-readable fake reaching here means the walk was driven with a\n\t * stream it cannot use, which fails loudly rather than silently. `terminal: false` leaves readline\n\t * as a line decoder and nothing else: the walk writes every prompt itself, through the same output\n\t * every other line goes to, so nothing is written twice and a secret is never echoed.\n\t */\n\t#openReadline(): { input: NodeJS.ReadableStream; terminal: boolean } {\n\t\t// Bind to a local first — a guard narrows a local, not a `#private` field access.\n\t\tconst input = this.#input\n\t\tif (!isReadable(input))\n\t\t\tthrow new TerminalError('DRIVER', 'Terminal fallback requires a readable input stream')\n\t\treturn { input, terminal: false }\n\t}\n}\n","import type { TerminalInterface } from '@src/core'\nimport type { TerminalOptions } from './types.js'\nimport { Terminal } from './Terminal.js'\n\n/**\n * Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,\n * beside the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the\n * broker parks a live form until somebody elsewhere answers it, a `Terminal` answers one here: it\n * walks the form's fields in schema order, drives each control's pure reducer over raw-mode stdin,\n * binds every answer through the form's own `fill`, and submits. It is the only impure part of the\n * terminal stack.\n *\n * @param options - See {@link TerminalOptions}\n * @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams\n *\n * @remarks\n * - **The form is the unit.** `ask` takes the caller's live form and returns its settled values. The\n * returned promise is that form's own `answer`, so a caller holding the form can await either one.\n * - **Every control renders.** The line-read controls are read as a line of text with their format cue,\n * `password` masks, `confirm` takes a key, `editor` takes a block, `select` and `checkbox` drive a\n * list, and an open `select` accepts a value its list does not offer.\n * - **The form decides.** A blank answer binds as absence, the form evaluates, and a refusal\n * re-walks only the fields the walk can still edit.\n * - **Injectable and guard-narrowed.** `input` and `output` default to `process.stdin` and\n * `process.stdout` but accept any stream of the declared minimal shape, resolved through their\n * guards rather than an assertion, so a test drives a whole form with a fake TTY that emits\n * scripted key chunks and records every rendered byte.\n * - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over\n * `node:readline` line input.\n *\n * @example Ask one form at this keyboard\n * ```ts\n * import { createForm } from '@orkestrel/form'\n * import { isTerminalError } from '@orkestrel/terminal'\n * import { createTerminal } from '@orkestrel/terminal/server'\n *\n * const terminal = createTerminal() // process.stdin / process.stdout by default\n * const form = createForm({\n * \tlabel: 'Sign up',\n * \tfields: [\n * \t\t{ control: 'text', name: 'name', label: 'Your name', rule: { required: true, minimum: 2 } },\n * \t\t{ control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },\n * \t\t{ control: 'password', name: 'token', label: 'Token' },\n * \t\t{ control: 'confirm', name: 'terms', label: 'Accept the terms', rule: { required: true } },\n * \t\t{\n * \t\t\tcontrol: 'select',\n * \t\t\tname: 'role',\n * \t\t\tlabel: 'Role',\n * \t\t\tchoices: [\n * \t\t\t\t{ value: 'admin', label: 'Admin' },\n * \t\t\t\t{ value: 'viewer', label: 'Viewer', help: 'read-only' },\n * \t\t\t],\n * \t\t},\n * \t],\n * })\n *\n * try {\n * \tconst values = await terminal.ask(form)\n * \tdeploy(values)\n * } catch (error) {\n * \t// Ctrl-c: the walk ended, and the form is still `editing` for whoever owns it.\n * \tif (isTerminalError(error) && error.code === 'CANCEL') form.destroy()\n * }\n * ```\n */\nexport function createTerminal(options?: TerminalOptions): TerminalInterface {\n\treturn new Terminal(options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["CSI","isObject","isFunction","RETURN","isNumber","isString","isBoolean","stdin","isStreamTarget","stdout","createStyler","createPromptTheme","NEWLINE","sanitizeDisplayText","parseValue","matchesAnswer","renderErrorLine","createInputState","reduceInput","createPasswordState","reducePassword","renderPromptHeader","createConfirmState","reduceConfirm","renderHintedHeader","createEditorState","reduceEditor","createSelectState","reduceSelect","createCheckboxState","reduceCheckbox","attempt","parseKey","TerminalError","createInterface"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/TerminalRead.ts","../../../src/server/TerminalWalk.ts","../../../src/server/Terminal.ts","../../../src/server/factories.ts"],"sourcesContent":["// Server-terminals constants — the cursor and line-clear ANSI sequences the interactive `Terminal`\n// driver writes to redraw a field view IN PLACE, plus the copy the walk shows around those views:\n// the per-control format hints, the readline-fallback prompts, and the marks for a locked field and\n// an unavailable choice. UPPER_SNAKE, every member exported. Every sequence is built from the\n// console module's own `CSI` prefix, so no raw control character appears in source and this module\n// declares no second copy of that primitive.\n\nimport type { FieldControl } from '@orkestrel/form'\nimport { CSI } from '@orkestrel/console'\n\n/**\n * Holds the cursor-up sequence template (`ESC[{count}A`) —\n * {@link import('./helpers.js').renderCursorUp} interpolates the `{count}` placeholder with the\n * number of lines to climb. Kept as a template so the count stays out of the constant.\n */\nexport const CSI_UP = `${CSI}{count}A`\n\n/**\n * Hides the cursor (`ESC[?25l`) — written before the driver starts redrawing a prompt so the cursor\n * does not flicker across the view during an in-place re-render; paired with {@link CURSOR_SHOW}.\n */\nexport const CURSOR_HIDE = `${CSI}?25l`\n\n/** Shows the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */\nexport const CURSOR_SHOW = `${CSI}?25h`\n\n/**\n * Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the whole previous view,\n * which a `select` or `checkbox` can spread over several lines, in one write before the new view is\n * rendered, so a redraw never leaves orphaned rows behind.\n */\nexport const CLEAR_DOWN = `${CSI}J`\n\n/**\n * Holds the numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback\n * appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are\n * printed numbered and the user types one number on a single readline line.\n */\nexport const FALLBACK_SELECT_HINT = 'Enter a number'\n\n/** Holds the comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */\nexport const FALLBACK_CHECKBOX_HINT = 'Enter numbers separated by commas'\n\n/** Holds the hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */\nexport const FALLBACK_EDITOR_HINT = '(end of input finishes)'\n\n/** Holds the hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */\nexport const FALLBACK_CONFIRM_HINT = '(y/n)'\n\n/**\n * Holds the format cue appended to a field's label for each control the walk reads as a line of text —\n * the terminal has no date picker, no color well, and no file chooser, so the accepted shape is\n * stated instead. A control with no entry needs none: `text` and `editor` accept any line,\n * `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by\n * format. The form's own rules still decide whether the typed value is acceptable.\n */\nexport const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string>>> = Object.freeze({\n\tnumber: '(number)',\n\tdate: '(YYYY-MM-DD)',\n\ttime: '(HH:MM)',\n\tdatetime: '(YYYY-MM-DDTHH:MM)',\n\tcolor: '(#rrggbb)',\n\tlist: '(entry)',\n\tfile: '(path)',\n})\n\n/** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */\nexport const FILE_HINT = 'One path per line, blank to finish'\n\n/** Holds the instruction a `list` field shows before its entries — one entry per line, and a blank line ends the list. */\nexport const LIST_HINT = 'One entry per line, blank to finish'\n\n/**\n * Holds the lead on the line listing an open `select`'s offered values, which a typed answer can\n * ignore.\n */\nexport const SUGGESTION_LEAD = 'Suggestions'\n\n/**\n * Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a\n * reader sees why one is missing from the list it heads.\n */\nexport const UNAVAILABLE_LEAD = 'Unavailable'\n\n/** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */\nexport const LOCKED_MARK = '(locked)'\n\n/**\n * States what a field is told when the walk read an answer the control cannot hold — a word typed into a\n * `number`, an off-list value typed into an open `select` whose choice is refused. The value binds\n * as absence and this message is invalidated onto the field, so the walk re-asks it with the reason\n * on screen.\n */\nexport const REFUSAL_MESSAGE = 'Enter a value this field accepts'\n","// Pure helpers for the server terminal driver — every function here is exported and unit-tested.\n// The families here are all total: the stream-boundary guards that narrow `process.stdin` / any\n// injected stream without an assertion; the cursor math the driver uses to redraw a field view IN\n// PLACE (count a view's lines, build the cursor-up sequence, assemble the reposition-and-clear\n// prefix); and the projections and line renderers the whole-form walk needs that no reducer owns —\n// a control read as text, a value shown read-only, the choices a field actually offers, and the\n// group, locked, suggestion, unavailable, and numbered-list lines. The impure driver only feeds\n// bytes into the reducers and writes the strings these helpers build. The output boundary is\n// narrowed by the console module's own `isStreamTarget`, which this module does not redeclare.\n\nimport type { FieldChoice, FieldValue, FormField, TextField } from '@orkestrel/form'\nimport type { PromptTheme } from '@src/core'\nimport type { StylerInterface } from '@orkestrel/console'\nimport type { InputStreamInterface } from './types.js'\nimport { RETURN } from '@src/core'\nimport {\n\tCLEAR_DOWN,\n\tCONTROL_HINTS,\n\tCSI_UP,\n\tLOCKED_MARK,\n\tSUGGESTION_LEAD,\n\tUNAVAILABLE_LEAD,\n} from './constants.js'\nimport { isBoolean, isFunction, isNumber, isObject, isString } from '@orkestrel/contract'\n\n/**\n * Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /\n * `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for\n * anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,\n * or a fake TTY a test injects) to the exact slice the driver reads, never an assertion.\n *\n * @remarks\n * Only `on` / `off` are required (the irreducible event seam); `setRawMode` / `resume` / `pause` /\n * `isTTY` are optional on {@link InputStreamInterface}, so their absence does not disqualify a stream\n * — a piped, non-TTY stream is still a valid input, one the driver reads through the readline\n * fallback rather than raw mode.\n *\n * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)\n * @returns True if `value` has callable `on` and `off`; false otherwise\n */\nexport function isInputStream(value: unknown): value is InputStreamInterface {\n\treturn (\n\t\tisObject(value) &&\n\t\t'on' in value &&\n\t\tisFunction(value.on) &&\n\t\t'off' in value &&\n\t\tisFunction(value.off)\n\t)\n}\n\n/**\n * Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard\n * checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface`\n * requires as its `input`. The non-TTY fallback narrows the resolved input stream through this\n * before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a\n * `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns\n * `false` for a minimal fake that isn't a full readable.\n *\n * @param value - The resolved input stream (or any value crossing the boundary)\n * @returns True if `value` has the readable methods readline needs; false otherwise\n */\nexport function isReadable(value: unknown): value is NodeJS.ReadableStream {\n\treturn (\n\t\tisObject(value) &&\n\t\t'read' in value &&\n\t\tisFunction(value.read) &&\n\t\t'pipe' in value &&\n\t\tisFunction(value.pipe)\n\t)\n}\n\n/**\n * Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and\n * exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to\n * choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a\n * live re-render; false selects the `node:readline` line-input fallback, because a piped or\n * non-terminal stream cannot enter raw mode. Total — never throws.\n *\n * @param input - The resolved {@link InputStreamInterface}\n * @returns True if the stream is a TTY with `setRawMode`; false otherwise\n */\nexport function supportsRawMode(input: InputStreamInterface): boolean {\n\treturn input.isTTY === true && isFunction(input.setRawMode)\n}\n\n/**\n * Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so\n * a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of\n * the in-place re-render: the driver records the line count of the view it wrote so the next redraw\n * knows how far up to move the cursor before overwriting. Total; an empty string is one empty line.\n *\n * @param view - The rendered (possibly multi-line, possibly ANSI-styled) view string\n * @returns The number of lines the view spans (always at least 1)\n */\nexport function lineCount(view: string): number {\n\treturn Iterator.from(view).reduce((lines, character) => lines + Number(character === '\\n'), 1)\n}\n\n/**\n * Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`),\n * or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A`\n * is a wasted write. The pure step the in-place re-render uses to climb back over the previous view\n * before clearing it. Total.\n *\n * @param count - How many lines to move the cursor up\n * @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`\n */\nexport function renderCursorUp(count: number): string {\n\tif (count <= 0) return ''\n\treturn `${CSI_UP.replace('{count}', String(count))}`\n}\n\n/**\n * Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place\n * — given the line count of the previous view, it moves the cursor up over those lines, returns it\n * to column 0, and erases everything from there to the end of the screen, so the next view is drawn\n * on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes\n * this immediately followed by the new view.\n *\n * @remarks\n * For the first render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so\n * the prefix is a carriage return + clear-down — the prompt draws from the current line. For a\n * subsequent render it climbs `previousLines - 1` lines (the cursor is on the last line of the prior\n * view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable\n * without a real terminal.\n *\n * @param previousLines - The line count of the view on screen (from {@link lineCount})\n * @returns The control-sequence prefix to write before the new view\n */\nexport function redrawPrefix(previousLines: number): string {\n\treturn `${renderCursorUp(previousLines - 1)}${RETURN}${CLEAR_DOWN}`\n}\n\n/**\n * Projects any field the walk reads as a line of text into the {@link TextField} the text reducer\n * takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`,\n * `datetime`, `color`, one `list` entry, and one `file` entry. The label carries that control's format cue from\n * {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The\n * projection carries no rule, because the authoritative form still evaluates the answer this line\n * binds; it exists only so one reducer covers every one of them.\n *\n * @param field - The field being read\n * @returns The text field the reducer renders for it\n *\n * @example\n * ```ts\n * fieldToText({ control: 'date', name: 'born', label: 'Birthday' })\n * // { control: 'text', name: 'born', label: 'Birthday (YYYY-MM-DD)' }\n * ```\n */\nexport function fieldToText(field: FormField): TextField {\n\tconst label = field.label ?? field.name\n\tconst hint = CONTROL_HINTS[field.control]\n\tconst seed = 'default' in field ? field.default : undefined\n\tconst text = isNumber(seed) ? String(seed) : seed\n\treturn {\n\t\tcontrol: 'text',\n\t\tname: field.name,\n\t\tlabel: hint === undefined ? label : `${label} ${hint}`,\n\t\t...(isString(text) ? { default: text } : {}),\n\t}\n}\n\n/**\n * Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as\n * `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders\n * as nothing, because a locked field nobody has answered has nothing to show.\n *\n * @param value - The answer the form holds for a field, or absence\n * @returns The text to render for it\n */\nexport function valueToText(value: FieldValue | undefined): string {\n\tif (value === undefined) return ''\n\tif (isString(value)) return value\n\tif (isNumber(value)) return String(value)\n\tif (isBoolean(value)) return value ? 'yes' : 'no'\n\treturn value.join(', ')\n}\n\n/**\n * Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled\n * choice's value at every door, including a fill, so the walk never puts one in front of the\n * cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.\n *\n * @param choices - The field's declared choices\n * @returns The choices the walk offers, in declared order\n */\nexport function filterEnabled(choices: readonly FieldChoice[]): readonly FieldChoice[] {\n\treturn choices.filter((choice) => choice.disabled !== true)\n}\n\n/**\n * Returns the choices a `select` or `checkbox` field shows but refuses — the complement of\n * {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees\n * why a declared choice is missing from it.\n *\n * @param choices - The field's declared choices\n * @returns The refused choices, in declared order\n */\nexport function filterDisabled(choices: readonly FieldChoice[]): readonly FieldChoice[] {\n\treturn choices.filter((choice) => choice.disabled === true)\n}\n\n/**\n * Renders the section header the walk writes when it enters a new field group, painted by the\n * `message` role.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param label - The group's declared label, falling back to its own name\n * @returns The rendered section header\n */\nexport function renderGroupHeader(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tlabel: string,\n): string {\n\treturn styler.render(theme.roles.message, label)\n}\n\n/**\n * Renders the read-only line a locked field shows — its label, the {@link LOCKED_MARK}, and the\n * answer the form already holds. The walk writes this instead of a prompt, because the field is\n * still validated and still submitted but must not be edited here.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param label - The field's label\n * @param value - The held answer, from {@link valueToText}\n * @returns The rendered line, with no trailing space when there is nothing to show\n */\nexport function renderLockedLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tlabel: string,\n\tvalue: string,\n): string {\n\tconst head = `${styler.render(theme.roles.muted, theme.icons.dot)} ${styler.render(theme.roles.message, label)} ${styler.render(theme.roles.hint, LOCKED_MARK)}`\n\treturn value.length === 0 ? head : `${head} ${styler.render(theme.roles.content, value)}`\n}\n\n/**\n * Renders the line listing an open select's offered values above its text prompt — a suggestion\n * list, because an open select admits an answer the list does not offer.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The choices the open select offers, from {@link filterEnabled}\n * @returns The rendered suggestion line\n */\nexport function renderSuggestionLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\tconst values = choices.map((choice) => choice.value).join(', ')\n\treturn styler.render(theme.roles.hint, `${SUGGESTION_LEAD}: ${values}`)\n}\n\n/**\n * Renders the line naming the choices a field shows but refuses, written above the list the walk\n * drives.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The refused choices, from {@link filterDisabled}\n * @returns The rendered unavailable line\n */\nexport function renderUnavailableLine(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\tconst labels = choices.map((choice) => choice.label).join(', ')\n\treturn styler.render(theme.roles.muted, `${UNAVAILABLE_LEAD}: ${labels}`)\n}\n\n/**\n * Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate\n * with arrow keys, so each offered choice is printed with the number the reader types back. One\n * line per choice, with no trailing newline.\n *\n * @param styler - The console styler that renders each role\n * @param theme - The resolved prompt theme\n * @param choices - The choices the walk offers, from {@link filterEnabled}\n * @returns The rendered list\n */\nexport function renderNumberedList(\n\tstyler: StylerInterface,\n\ttheme: PromptTheme,\n\tchoices: readonly FieldChoice[],\n): string {\n\treturn choices\n\t\t.map(\n\t\t\t(choice, index) =>\n\t\t\t\t` ${styler.render(theme.roles.muted, `${String(index + 1)})`)} ${styler.render(theme.roles.content, choice.label)}`,\n\t\t)\n\t\t.join('\\n')\n}\n","/** Holds reducer state and the rendered line count for one terminal read. */\nexport class TerminalRead<TState> {\n\t#state: TState\n\t#lines: number\n\n\tconstructor(state: TState, lines: number) {\n\t\tthis.#state = state\n\t\tthis.#lines = lines\n\t}\n\n\tget state(): TState {\n\t\treturn this.#state\n\t}\n\n\tget lines(): number {\n\t\treturn this.#lines\n\t}\n\n\tadvance(state: TState): void {\n\t\tthis.#state = state\n\t}\n\n\tpaint(lines: number): void {\n\t\tthis.#lines = lines\n\t}\n}\n","/** Tracks the group whose heading one form walk has displayed. */\nexport class TerminalWalk {\n\t#group: string | undefined\n\n\tget group(): string | undefined {\n\t\treturn this.#group\n\t}\n\n\tenter(group: string | undefined): void {\n\t\tthis.#group = group\n\t}\n}\n","import type {\n\tFieldChoice,\n\tFieldError,\n\tFieldValue,\n\tFormField,\n\tFormInterface,\n\tFormValues,\n} from '@orkestrel/form'\nimport type {\n\tCheckboxField,\n\tConfirmField,\n\tEditorField,\n\tFileField,\n\tListField,\n\tPasswordField,\n\tSelectField,\n} from '@orkestrel/form'\nimport type { KeyEvent, PromptStep, PromptTheme, TerminalInterface } from '@src/core'\nimport type { StylerInterface } from '@orkestrel/console'\nimport type { StreamTargetInterface } from '@orkestrel/console/server'\nimport type { Interface } from 'node:readline'\nimport type { InputStreamInterface, TerminalOptions } from './types.js'\nimport {\n\tcreateCheckboxState,\n\tcreateConfirmState,\n\tcreateEditorState,\n\tcreateInputState,\n\tcreatePasswordState,\n\tcreatePromptTheme,\n\tcreateSelectState,\n\tNEWLINE,\n\tparseKey,\n\treduceCheckbox,\n\treduceConfirm,\n\treduceEditor,\n\treduceInput,\n\treducePassword,\n\treduceSelect,\n\trenderErrorLine,\n\trenderHintedHeader,\n\trenderPromptHeader,\n\tsanitizeDisplayText,\n\tTerminalError,\n} from '@src/core'\nimport { matchesAnswer, parseValue } from '@orkestrel/form'\nimport { createStyler } from '@orkestrel/console'\nimport { isStreamTarget } from '@orkestrel/console/server'\nimport { createInterface } from 'node:readline'\nimport { stdin, stdout } from 'node:process'\nimport { attempt } from '@orkestrel/contract'\nimport { TerminalRead } from './TerminalRead.js'\nimport { TerminalWalk } from './TerminalWalk.js'\nimport {\n\tCURSOR_HIDE,\n\tCURSOR_SHOW,\n\tFALLBACK_CHECKBOX_HINT,\n\tFALLBACK_CONFIRM_HINT,\n\tFALLBACK_EDITOR_HINT,\n\tFALLBACK_SELECT_HINT,\n\tFILE_HINT,\n\tLIST_HINT,\n\tREFUSAL_MESSAGE,\n} from './constants.js'\nimport {\n\tfieldToText,\n\tfilterDisabled,\n\tfilterEnabled,\n\tisInputStream,\n\tisReadable,\n\tlineCount,\n\tredrawPrefix,\n\trenderGroupHeader,\n\trenderLockedLine,\n\trenderNumberedList,\n\trenderSuggestionLine,\n\trenderUnavailableLine,\n\tsupportsRawMode,\n\tvalueToText,\n} from './helpers.js'\n\n/**\n * Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive\n * form driver, and the only impure part of the terminal stack. {@link ask} walks one form's fields\n * in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching pure reducer,\n * renders each returned view in place, binds every answer through the form's own `fill`, and\n * re-asks what the form refused. It owns no form logic: the schema, the rules, the values, and the\n * settlement all belong to the form it is given, and this class owns only raw mode, the cursor, and\n * the re-render.\n *\n * @remarks\n * See {@link TerminalInterface} for the driving contract. The walk itself:\n *\n * - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, each `list`\n * entry, and each `file` entry are read as one line of text through {@link fieldToText}, which\n * appends that control's format cue to the label; a `list` reads one entry per line until a blank\n * one and keeps its declared `default` when none is typed. `password`, `confirm`, `editor`,\n * `select`, and `checkbox` each drive their own reducer. An open `select` is a suggestion list\n * plus a typed line, because `open` means the answer need not come from the list.\n * - **The binding projects through `matchesAnswer`.** Every answer is filled as\n * `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the\n * control's own shape, so a bare return on a field with no default binds as absence and the\n * form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and\n * invalidates the field, so the walk asks again with the reason on screen.\n * - **Visibility is honored.** A `hidden` field and a field in `form.disabled` are skipped; a\n * `locked` field renders read-only; entering a new group writes its label as a section header.\n * - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring\n * fields the walk can edit and submits again. When every erroring field is one the walk cannot\n * edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could\n * not change the answer.\n * - **Raw-mode leak-free.** Raw mode is entered once per field and always cleaned up: on submit, on\n * cancel, on a throw, and when the form is abandoned under an active read.\n * - **Non-TTY fallback.** When `input` is not a TTY, the same walk runs over `node:readline` line\n * input: `select` and `checkbox` print a numbered list, and `editor` reads to end of input.\n */\nexport class Terminal implements TerminalInterface {\n\treadonly #input: InputStreamInterface\n\treadonly #output: StreamTargetInterface\n\treadonly #styler: StylerInterface\n\treadonly #theme: PromptTheme\n\treadonly #handlers = new Map<object, (chunk: string | Uint8Array) => void>()\n\treadonly #queue: string[] = []\n\t#interface: Interface | undefined\n\t#taker: ((line: string | undefined) => void) | undefined\n\t#ended = false\n\n\tconstructor(options?: TerminalOptions) {\n\t\t// Resolve each stream through its guard: a present, well-shaped injected stream is used as is;\n\t\t// otherwise the real process stream — no assertion, and an absent option falls through.\n\t\tthis.#input = isInputStream(options?.input) ? options.input : stdin\n\t\tthis.#output = isStreamTarget(options?.output) ? options.output : stdout\n\t\tthis.#styler = createStyler()\n\t\tthis.#theme = createPromptTheme(options?.theme)\n\t}\n\n\t// === The contract\n\n\tasync ask(form: FormInterface): Promise<FormValues> {\n\t\ttry {\n\t\t\tthis.#report(form, form.errors)\n\t\t\tawait this.#walk(form, form.schema.fields)\n\t\t\tfor (;;) {\n\t\t\t\tif (form.status !== 'editing') return await form.answer\n\t\t\t\tconst result = form.submit()\n\t\t\t\tif (result.success) return await form.answer\n\t\t\t\tthis.#report(form, result.error)\n\t\t\t\tconst again = this.#collectEditable(form, result.error)\n\t\t\t\t// Nothing the walk can edit, or an input stream that has already ended: asking again would\n\t\t\t\t// render the same fields and read the same answers forever, so abandon the form instead.\n\t\t\t\t// The failures are on screen either way, so the reader sees what could not be answered.\n\t\t\t\tif (again.length === 0 || this.#ended) {\n\t\t\t\t\tform.destroy()\n\t\t\t\t\treturn await form.answer\n\t\t\t\t}\n\t\t\t\tawait this.#walk(form, again)\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#close()\n\t\t}\n\t}\n\n\t// === The walk\n\n\t/**\n\t * Asks `fields` in the order given, skipping what the walk must not touch and rendering what it\n\t * must not edit. Returns early the moment the form stops being editable, so an abandoned form\n\t * ends the walk between fields as well as under an active read.\n\t */\n\tasync #walk(form: FormInterface, fields: readonly FormField[]): Promise<void> {\n\t\tconst walk = new TerminalWalk()\n\t\tfor (const field of fields) {\n\t\t\tif (form.status !== 'editing') return\n\t\t\tif (field.hidden === true) continue\n\t\t\tif (form.disabled.has(field.name)) continue\n\t\t\tif (field.group !== walk.group) {\n\t\t\t\twalk.enter(field.group)\n\t\t\t\tif (walk.group !== undefined) this.#writeGroup(form, walk.group)\n\t\t\t}\n\t\t\tif (field.locked === true) {\n\t\t\t\tthis.#writeLocked(form, field)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst raw = await this.#read(form, field)\n\t\t\tif (form.status !== 'editing') return\n\t\t\tthis.#bind(form, field, raw)\n\t\t}\n\t}\n\n\t/** Writes a group's label as a section header, resolving the schema's declared label and falling back to the group's own name. */\n\t#writeGroup(form: FormInterface, name: string): void {\n\t\tconst label = form.schema.groups?.find((group) => group.name === name)?.label ?? name\n\t\tthis.#output.write(\n\t\t\t`${NEWLINE}${renderGroupHeader(this.#styler, this.#theme, sanitizeDisplayText(label))}${NEWLINE}`,\n\t\t)\n\t}\n\n\t/** Renders a locked field read-only — its label, its mark, and the answer the form already holds. */\n\t#writeLocked(form: FormInterface, field: FormField): void {\n\t\tconst line = renderLockedLine(\n\t\t\tthis.#styler,\n\t\t\tthis.#theme,\n\t\t\tsanitizeDisplayText(field.label ?? field.name),\n\t\t\tsanitizeDisplayText(valueToText(form.values[field.name])),\n\t\t)\n\t\tthis.#output.write(`${line}${NEWLINE}`)\n\t}\n\n\t/** Reads one field through the reducer its control names, resolving the raw answer the binding projects. */\n\t#read(form: FormInterface, field: FormField): Promise<FieldValue | undefined> {\n\t\tif (field.control === 'password') return this.#askPassword(form, field)\n\t\tif (field.control === 'confirm') return this.#confirm(form, field)\n\t\tif (field.control === 'editor') return this.#askEditor(form, field)\n\t\tif (field.control === 'select') return this.#select(form, field)\n\t\tif (field.control === 'checkbox') return this.#checkbox(form, field)\n\t\tif (field.control === 'list') return this.#list(form, field)\n\t\tif (field.control === 'file') return this.#file(form, field)\n\t\treturn this.#askText(form, field)\n\t}\n\n\t/**\n\t * Binds one raw answer to the form — the single binding, and the only place this driver writes a\n\t * value. `parseValue` coerces the raw answer to the control's own shape and `matchesAnswer`\n\t * projects a blank one to absence, which is what keeps `required` refusing a bare return. A raw\n\t * answer the control cannot hold binds as absence and invalidates the field, so the field comes\n\t * back on the next pass carrying the reason rather than vanishing silently.\n\t */\n\t#bind(form: FormInterface, field: FormField, raw: FieldValue | undefined): void {\n\t\tform.touch(field.name)\n\t\tconst value = raw === undefined ? undefined : parseValue(field, raw)\n\t\tform.fill(field.name, matchesAnswer(value) ? value : undefined)\n\t\tif (value === undefined && matchesAnswer(raw)) form.invalidate(field.name, REFUSAL_MESSAGE)\n\t}\n\n\t/** 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. */\n\t#collectEditable(form: FormInterface, errors: readonly FieldError[]): readonly FormField[] {\n\t\tconst fields: FormField[] = []\n\t\tfor (const error of errors) {\n\t\t\tconst field = form.field(error.field)\n\t\t\tif (field === undefined) continue\n\t\t\tif (field.hidden === true || field.locked === true) continue\n\t\t\tif (form.disabled.has(field.name)) continue\n\t\t\tif (fields.some((held) => held.name === field.name)) continue\n\t\t\tfields.push(field)\n\t\t}\n\t\treturn fields\n\t}\n\n\t/** Writes every supplied failure against its field's label. */\n\t#report(form: FormInterface, errors: readonly FieldError[]): void {\n\t\tfor (const error of errors) {\n\t\t\tconst label = sanitizeDisplayText(form.field(error.field)?.label ?? error.field)\n\t\t\tconst message = sanitizeDisplayText(error.message)\n\t\t\tconst line = renderErrorLine(this.#styler, this.#theme, `${label}: ${message}`)\n\t\t\tthis.#output.write(`${line}${NEWLINE}`)\n\t\t}\n\t}\n\n\t// === The controls\n\n\t/** Reads a field as one line of text — `text` itself and the controls a terminal has no widget for. */\n\t#askText(form: FormInterface, field: FormField): Promise<string> {\n\t\tconst state = createInputState(fieldToText(field), this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceInput)\n\t\treturn this.#line(form, state.message, state.default)\n\t}\n\n\t/** Reads a secret — masked live in raw mode, and read without echo through readline on a stream that cannot enter it. */\n\t#askPassword(form: FormInterface, field: PasswordField): Promise<string> {\n\t\tconst state = createPasswordState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reducePassword)\n\t\treturn this.#prompt(form, renderPromptHeader(this.#styler, this.#theme, state.message))\n\t}\n\n\t/**\n\t * Reads a yes or no. The fallback accepts `y` / `yes` / `n` / `no` in any case and takes the\n\t * field's default for a bare line; anything else is returned as typed, so the binding refuses it\n\t * and the walk asks again rather than silently reading it as no.\n\t */\n\tasync #confirm(form: FormInterface, field: ConfirmField): Promise<FieldValue> {\n\t\tconst state = createConfirmState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceConfirm)\n\t\tconst header = renderHintedHeader(\n\t\t\tthis.#styler,\n\t\t\tthis.#theme,\n\t\t\tstate.message,\n\t\t\tFALLBACK_CONFIRM_HINT,\n\t\t)\n\t\tconst line = await this.#prompt(form, header)\n\t\tconst answer = line.trim().toLowerCase()\n\t\tif (answer.length === 0) return state.default\n\t\tif (answer === 'y' || answer === 'yes') return true\n\t\tif (answer === 'n' || answer === 'no') return false\n\t\treturn line.trim()\n\t}\n\n\t/** Reads text over many lines — ctrl-d finishes in raw mode, and end of input finishes on a piped stream. */\n\t#askEditor(form: FormInterface, field: EditorField): Promise<string> {\n\t\tconst state = createEditorState(field, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceEditor)\n\t\treturn this.#block(form, state.message, state.default)\n\t}\n\n\t/**\n\t * Reads one choice. A disabled choice is named above the list and never offered, because the form\n\t * refuses its value at every door. An open select is a suggestion list plus a typed line, since\n\t * `open` admits an answer the list does not offer; a closed select with nothing left to offer\n\t * resolves absence and lets the form's own rules report it.\n\t */\n\tasync #select(form: FormInterface, field: SelectField): Promise<FieldValue | undefined> {\n\t\tconst choices = filterEnabled(field.choices)\n\t\tthis.#writeUnavailable(field.choices)\n\t\tif (field.open === true) {\n\t\t\tif (choices.length > 0) {\n\t\t\t\tconst display = choices.map((choice) => ({\n\t\t\t\t\t...choice,\n\t\t\t\t\tvalue: sanitizeDisplayText(choice.value),\n\t\t\t\t}))\n\t\t\t\tthis.#output.write(`${renderSuggestionLine(this.#styler, this.#theme, display)}${NEWLINE}`)\n\t\t\t}\n\t\t\treturn this.#askText(form, field)\n\t\t}\n\t\tif (choices.length === 0) return undefined\n\t\tconst state = createSelectState({ ...field, choices }, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceSelect)\n\t\tthis.#writeList(state.message, choices)\n\t\tconst line = await this.#prompt(form, this.#formatHint(`${FALLBACK_SELECT_HINT}:`))\n\t\treturn choices[Number.parseInt(line.trim(), 10) - 1]?.value\n\t}\n\n\t/**\n\t * Reads any number of choices. Disabled choices are named above the list and never offered, so a\n\t * box the form would refuse can never be ticked. An empty answer is an answered \"none of them\",\n\t * which is what `matchesAnswer` says an empty list means.\n\t */\n\tasync #checkbox(form: FormInterface, field: CheckboxField): Promise<FieldValue> {\n\t\tconst choices = filterEnabled(field.choices)\n\t\tthis.#writeUnavailable(field.choices)\n\t\tconst state = createCheckboxState({ ...field, choices }, this.#styler, this.#theme)\n\t\tif (supportsRawMode(this.#input)) return this.#drive(form, state, reduceCheckbox)\n\t\tthis.#writeList(state.message, choices)\n\t\tconst line = await this.#prompt(form, this.#formatHint(`${FALLBACK_CHECKBOX_HINT}:`))\n\t\treturn line\n\t\t\t.split(',')\n\t\t\t.map((part) => choices[Number.parseInt(part.trim(), 10) - 1]?.value)\n\t\t\t.filter((value): value is string => value !== undefined)\n\t}\n\n\t/**\n\t * Reads file paths — names only, because bytes never enter a form. A `multiple` field collects one\n\t * path per line until a blank one, and a single field takes one. No path at all is absence rather\n\t * than an empty list, because the reader answered a blank line.\n\t */\n\tasync #file(form: FormInterface, field: FileField): Promise<FieldValue | undefined> {\n\t\tif (field.multiple === true) this.#output.write(`${this.#formatHint(FILE_HINT)}${NEWLINE}`)\n\t\tconst paths = await this.#collect(form, field, field.multiple === true)\n\t\treturn paths.length > 0 ? paths : undefined\n\t}\n\n\t// A blank list keeps its declared default or absence so the form's own `required` rule decides.\n\tasync #list(form: FormInterface, field: ListField): Promise<FieldValue | undefined> {\n\t\tthis.#output.write(`${this.#formatHint(LIST_HINT)}${NEWLINE}`)\n\t\tconst entries = await this.#collect(form, field, true)\n\t\treturn entries.length > 0 ? entries : field.default\n\t}\n\n\t// Let the text reader drain buffered entries before treating EOF as a blank terminator.\n\tasync #collect(form: FormInterface, field: FormField, many: boolean): Promise<readonly string[]> {\n\t\tconst entries: string[] = []\n\t\tfor (;;) {\n\t\t\tconst entry = await this.#askText(form, field)\n\t\t\tif (entry.length === 0) break\n\t\t\tentries.push(entry)\n\t\t\tif (!many) break\n\t\t}\n\t\treturn entries\n\t}\n\n\t/** Names the choices a field shows but refuses, above the list the walk drives. */\n\t#writeUnavailable(choices: readonly FieldChoice[]): void {\n\t\tconst refused = filterDisabled(choices)\n\t\tif (refused.length === 0) return\n\t\tthis.#output.write(`${renderUnavailableLine(this.#styler, this.#theme, refused)}${NEWLINE}`)\n\t}\n\n\t/** Writes a field's header above its numbered choice list — the non-TTY presentation of a choice field. */\n\t#writeList(message: string, choices: readonly FieldChoice[]): void {\n\t\tconst header = renderPromptHeader(this.#styler, this.#theme, message)\n\t\tconst list = renderNumberedList(this.#styler, this.#theme, choices)\n\t\tthis.#output.write(`${header}${NEWLINE}${list}${NEWLINE}`)\n\t}\n\n\t/** Paints one line of supplementary instruction through the `hint` role. */\n\t#formatHint(text: string): string {\n\t\treturn this.#styler.render(this.#theme.roles.hint, text)\n\t}\n\n\t// === The raw-mode kernel\n\n\t/**\n\t * Isolates the irreducible Node raw-mode primitive — the ONLY place raw mode is touched. Switches the input\n\t * into raw mode (each keypress delivered immediately, no echo), resumes its flow, and subscribes\n\t * `handler` to `'data'`. The paired {@link #leaveRaw} operation unsubscribes the exact handler,\n\t * leaves raw mode, and pauses the stream on submit, cancel, abandon, or throw.\n\t */\n\t#enterRaw(token: object, handler: (chunk: string | Uint8Array) => void): void {\n\t\tthis.#handlers.set(token, handler)\n\t\tthis.#input.setRawMode?.(true)\n\t\tthis.#input.resume?.()\n\t\tthis.#input.on('data', handler)\n\t}\n\n\t#leaveRaw(token: object): void {\n\t\tconst handler = this.#handlers.get(token)\n\t\tif (handler === undefined) return\n\t\tthis.#handlers.delete(token)\n\t\tthis.#input.off('data', handler)\n\t\tthis.#input.setRawMode?.(false)\n\t\tthis.#input.pause?.()\n\t}\n\n\t/**\n\t * Drives ONE field over raw-mode stdin — the generic engine every interactive control shares.\n\t * Renders the reducer's initial view, enters raw mode once, and on each keypress runs `parseKey` →\n\t * `reduce` → an in-place re-render; on `submit` it cleans up and resolves the reducer's value, on\n\t * `cancel` (ctrl-c) it cleans up and rejects a {@link TerminalError} coded `CANCEL`. Abandoning\n\t * the form ends the read too, through the rejection of its own `answer`. Raw mode is entered\n\t * exactly once and cleaned up on every exit path, so no raw mode and no `'data'` listener ever\n\t * leaks. The cursor is hidden for the duration and restored on exit.\n\t */\n\t#drive<T, S>(\n\t\tform: FormInterface,\n\t\tinitial: S,\n\t\treduce: (state: S, key: KeyEvent) => PromptStep<T, S>,\n\t): Promise<T> {\n\t\treturn new Promise<T>((resolve, reject) => {\n\t\t\t// Render the first view, tracking how many lines it spans so the next redraw climbs over them.\n\t\t\tthis.#output.write(CURSOR_HIDE)\n\t\t\tconst firstView = attempt(() => {\n\t\t\t\tconst view = reduce(initial, parseKey('')).view\n\t\t\t\tthis.#output.write(view)\n\t\t\t\treturn view\n\t\t\t})\n\t\t\tif (!firstView.success) {\n\t\t\t\t// Raw mode was never entered yet, but the cursor was hidden — restore it before rejecting.\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\treject(firstView.error)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst token = {}\n\t\t\tconst handler = this.#createHandler(\n\t\t\t\ttoken,\n\t\t\t\tinitial,\n\t\t\t\tlineCount(firstView.value),\n\t\t\t\treduce,\n\t\t\t\tresolve,\n\t\t\t\treject,\n\t\t\t)\n\t\t\tthis.#enterRaw(token, handler)\n\t\t\t// An abandoned form ends the read: its own `answer` rejects, so release the terminal and fail\n\t\t\t// this field with the form's error. A read that already finished holds no token and is a no-op.\n\t\t\tvoid form.answer.catch((error: unknown) => this.#interrupt(token, reject, error))\n\t\t})\n\t}\n\n\t#createHandler<T, S>(\n\t\ttoken: object,\n\t\tinitial: S,\n\t\tinitialLines: number,\n\t\treduce: (state: S, key: KeyEvent) => PromptStep<T, S>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void,\n\t): (chunk: string | Uint8Array) => void {\n\t\tconst read = new TerminalRead(initial, initialLines)\n\t\treturn (chunk) => {\n\t\t\ttry {\n\t\t\t\tconst step = reduce(read.state, parseKey(chunk))\n\t\t\t\tread.advance(step.state)\n\t\t\t\tif (step.status === 'active') {\n\t\t\t\t\tthis.#render(step.view, read.lines)\n\t\t\t\t\tread.paint(lineCount(step.view))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Terminal step (submit / cancel): paint the final committed view, then tear down.\n\t\t\t\tthis.#render(step.view, read.lines)\n\t\t\t\tthis.#leaveRaw(token)\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\tif (step.status === 'cancel') reject(new TerminalError('CANCEL', 'Prompt cancelled'))\n\t\t\t\telse if (step.status === 'submit' && step.value !== undefined) resolve(step.value)\n\t\t\t\telse reject(new TerminalError('DRIVER', 'submit produced no value'))\n\t\t\t} catch (error) {\n\t\t\t\t// A throw inside the reducer/styler: tear down raw mode + the listener, restore the cursor\n\t\t\t\t// exactly as the normal exit path does, then reject.\n\t\t\t\tthis.#leaveRaw(token)\n\t\t\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\t\t\treject(error)\n\t\t\t}\n\t\t}\n\t}\n\n\t/** 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. */\n\t#interrupt(token: object, reject: (reason?: unknown) => void, error: unknown): void {\n\t\tif (!this.#handlers.has(token)) return\n\t\tthis.#leaveRaw(token)\n\t\tthis.#output.write(`${CURSOR_SHOW}${NEWLINE}`)\n\t\treject(error)\n\t}\n\n\t/** 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}). */\n\t#render(view: string, previousLines: number): void {\n\t\tthis.#output.write(`${redrawPrefix(previousLines)}${view}`)\n\t}\n\n\t// === Non-TTY fallbacks (node:readline line input)\n\n\t/** Reads one line for a text-shaped field, taking the field's default when the line is bare. */\n\tasync #line(form: FormInterface, message: string, seed: string): Promise<string> {\n\t\tconst answer = await this.#prompt(form, renderPromptHeader(this.#styler, this.#theme, message))\n\t\treturn answer.length > 0 ? answer : seed\n\t}\n\n\t/**\n\t * Reads a whole block for an `editor` field — a piped stream has no ctrl-d keypress, so end of\n\t * input is the terminator. It drains every remaining line, which is why nothing after an `editor`\n\t * field can be asked on the same stream.\n\t */\n\tasync #block(form: FormInterface, message: string, seed: string): Promise<string> {\n\t\tconst header = renderHintedHeader(this.#styler, this.#theme, message, FALLBACK_EDITOR_HINT)\n\t\tthis.#output.write(`${header}${NEWLINE}`)\n\t\tconst collected: string[] = []\n\t\tfor (;;) {\n\t\t\tconst line = await this.#next(form)\n\t\t\tif (line === undefined) break\n\t\t\tcollected.push(line)\n\t\t}\n\t\tconst text = collected.join('\\n')\n\t\treturn text.length > 0 ? text : seed\n\t}\n\n\t/** Writes one field's header and reads back the line the reader answers it with, or `''` once the stream has ended. */\n\tasync #prompt(form: FormInterface, header: string): Promise<string> {\n\t\tthis.#output.write(`${header} `)\n\t\tconst line = await this.#next(form)\n\t\treturn line ?? ''\n\t}\n\n\t/**\n\t * Takes the next line the input carries — from the buffer when the reader has already read ahead,\n\t * and otherwise by waiting for one. It resolves ABSENCE once the stream has ended, so a walk over\n\t * an exhausted stream settles instead of waiting forever, and it rejects when the form is\n\t * abandoned, so an unanswerable read ends with the form rather than outliving it.\n\t */\n\t#next(form: FormInterface): Promise<string | undefined> {\n\t\tconst buffered = this.#queue.shift()\n\t\tif (buffered !== undefined) return Promise.resolve(buffered)\n\t\tif (this.#ended) return Promise.resolve(undefined)\n\t\tthis.#startReader()\n\t\treturn new Promise<string | undefined>((resolve, reject) => {\n\t\t\tthis.#taker = resolve\n\t\t\tvoid form.answer.catch((error: unknown) => {\n\t\t\t\tif (this.#taker !== resolve) return\n\t\t\t\tthis.#taker = undefined\n\t\t\t\treject(error)\n\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Opens the walk's ONE readline interface, or keeps the open one. A whole form is many questions\n\t * over one stream, and an interface reads ahead: a fresh interface per question would swallow\n\t * every line it read past the one it was asked for, so the walk holds a single reader and buffers\n\t * what arrives early. It is closed when the walk ends, so the process is free to exit.\n\t */\n\t#startReader(): void {\n\t\tif (this.#interface !== undefined) return\n\t\tconst rl = createInterface(this.#openReadline())\n\t\trl.on('line', (line) => this.#accept(line))\n\t\trl.on('close', () => this.#finish())\n\t\tthis.#interface = rl\n\t}\n\n\t/** Hands a line to whoever is waiting for it, or buffers it for the next question. */\n\t#accept(line: string): void {\n\t\tconst taker = this.#taker\n\t\tif (taker === undefined) {\n\t\t\tthis.#queue.push(line)\n\t\t\treturn\n\t\t}\n\t\tthis.#taker = undefined\n\t\ttaker(line)\n\t}\n\n\t/** Records that no further line can arrive and releases whoever was waiting for one. */\n\t#finish(): void {\n\t\tthis.#ended = true\n\t\tconst taker = this.#taker\n\t\tif (taker === undefined) return\n\t\tthis.#taker = undefined\n\t\ttaker(undefined)\n\t}\n\n\t/** Closes the walk's reader and releases a waiting question — the walk is over, so the stream must stop holding the process open. */\n\t#close(): void {\n\t\tconst taker = this.#taker\n\t\tthis.#taker = undefined\n\t\tif (taker !== undefined) taker(undefined)\n\t\tconst rl = this.#interface\n\t\tif (rl === undefined) return\n\t\tthis.#interface = undefined\n\t\t// Drop the close listener first: this close is the walk ending, not the stream ending, and\n\t\t// recording it as end of input would refuse the next walk its input.\n\t\trl.removeAllListeners('close')\n\t\trl.close()\n\t}\n\n\t/**\n\t * Narrows the resolved input to the `node:readline` `createInterface` boundary, never an assertion.\n\t * The fallback only runs on a real piped `process.stdin` (or a `PassThrough` a test injects), both\n\t * genuine readables; a minimal non-readable fake reaching here means the walk was driven with a\n\t * stream it cannot use, which fails loudly rather than silently. `terminal: false` leaves readline\n\t * as a line decoder and nothing else: the walk writes every prompt itself, through the same output\n\t * every other line goes to, so nothing is written twice and a secret is never echoed.\n\t */\n\t#openReadline(): { input: NodeJS.ReadableStream; terminal: boolean } {\n\t\t// Bind to a local first — a guard narrows a local, not a `#private` field access.\n\t\tconst input = this.#input\n\t\tif (!isReadable(input))\n\t\t\tthrow new TerminalError('DRIVER', 'Terminal fallback requires a readable input stream')\n\t\treturn { input, terminal: false }\n\t}\n}\n","import type { TerminalInterface } from '@src/core'\nimport type { TerminalOptions } from './types.js'\nimport { Terminal } from './Terminal.js'\n\n/**\n * Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,\n * beside the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the\n * broker parks a live form until somebody elsewhere answers it, a `Terminal` answers one here: it\n * walks the form's fields in schema order, drives each control's pure reducer over raw-mode stdin,\n * binds every answer through the form's own `fill`, and submits. It is the only impure part of the\n * terminal stack.\n *\n * @param options - See {@link TerminalOptions}\n * @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams\n *\n * @remarks\n * - **The form is the unit.** `ask` takes the caller's live form and returns its settled values. The\n * returned promise is that form's own `answer`, so a caller holding the form can await either one.\n * - **Every control renders.** The line-read controls are read as a line of text with their format cue,\n * `password` masks, `confirm` takes a key, `editor` takes a block, `select` and `checkbox` drive a\n * list, and an open `select` accepts a value its list does not offer.\n * - **The form decides.** A blank answer binds as absence, the form evaluates, and a refusal\n * re-walks only the fields the walk can still edit.\n * - **Injectable and guard-narrowed.** `input` and `output` default to `process.stdin` and\n * `process.stdout` but accept any stream of the declared minimal shape, resolved through their\n * guards rather than an assertion, so a test drives a whole form with a fake TTY that emits\n * scripted key chunks and records every rendered byte.\n * - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over\n * `node:readline` line input.\n *\n * @example Ask one form at this keyboard\n * ```ts\n * import { createForm } from '@orkestrel/form'\n * import { isTerminalError } from '@orkestrel/terminal'\n * import { createTerminal } from '@orkestrel/terminal/server'\n *\n * const terminal = createTerminal() // process.stdin / process.stdout by default\n * const form = createForm({\n * \tlabel: 'Sign up',\n * \tfields: [\n * \t\t{ control: 'text', name: 'name', label: 'Your name', rule: { required: true, minimum: 2 } },\n * \t\t{ control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },\n * \t\t{ control: 'password', name: 'token', label: 'Token' },\n * \t\t{ control: 'confirm', name: 'terms', label: 'Accept the terms', rule: { required: true } },\n * \t\t{\n * \t\t\tcontrol: 'select',\n * \t\t\tname: 'role',\n * \t\t\tlabel: 'Role',\n * \t\t\tchoices: [\n * \t\t\t\t{ value: 'admin', label: 'Admin' },\n * \t\t\t\t{ value: 'viewer', label: 'Viewer', help: 'read-only' },\n * \t\t\t],\n * \t\t},\n * \t],\n * })\n *\n * try {\n * \tconst values = await terminal.ask(form)\n * \tdeploy(values)\n * } catch (error) {\n * \t// Ctrl-c: the walk ended, and the form is still `editing` for whoever owns it.\n * \tif (isTerminalError(error) && error.code === 'CANCEL') form.destroy()\n * }\n * ```\n */\nexport function createTerminal(options?: TerminalOptions): TerminalInterface {\n\treturn new Terminal(options)\n}\n"],"mappings":"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"}
|
|
@@ -125,7 +125,7 @@ export declare const FALLBACK_SELECT_HINT = "Enter a number";
|
|
|
125
125
|
/**
|
|
126
126
|
* Projects any field the walk reads as a line of text into the {@link TextField} the text reducer
|
|
127
127
|
* takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`, `time`,
|
|
128
|
-
* `datetime`, `color`, and one `file` entry. The label carries that control's format cue from
|
|
128
|
+
* `datetime`, `color`, one `list` entry, and one `file` entry. The label carries that control's format cue from
|
|
129
129
|
* {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
|
|
130
130
|
* projection carries no rule, because the authoritative form still evaluates the answer this line
|
|
131
131
|
* binds; it exists only so one reducer covers every one of them.
|
|
@@ -239,6 +239,9 @@ export declare function isReadable(value: unknown): value is NodeJS.ReadableStre
|
|
|
239
239
|
*/
|
|
240
240
|
export declare function lineCount(view: string): number;
|
|
241
241
|
|
|
242
|
+
/** Holds the instruction a `list` field shows before its entries — one entry per line, and a blank line ends the list. */
|
|
243
|
+
export declare const LIST_HINT = "One entry per line, blank to finish";
|
|
244
|
+
|
|
242
245
|
/** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
|
|
243
246
|
export declare const LOCKED_MARK = "(locked)";
|
|
244
247
|
|
|
@@ -368,11 +371,12 @@ export declare function supportsRawMode(input: InputStreamInterface): boolean;
|
|
|
368
371
|
* @remarks
|
|
369
372
|
* See {@link TerminalInterface} for the driving contract. The walk itself:
|
|
370
373
|
*
|
|
371
|
-
* - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`,
|
|
372
|
-
* each `file` entry are read as one line of text through {@link fieldToText}, which
|
|
373
|
-
* control's format cue to the label
|
|
374
|
-
*
|
|
375
|
-
* `
|
|
374
|
+
* - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, each `list`
|
|
375
|
+
* entry, and each `file` entry are read as one line of text through {@link fieldToText}, which
|
|
376
|
+
* appends that control's format cue to the label; a `list` reads one entry per line until a blank
|
|
377
|
+
* one and keeps its declared `default` when none is typed. `password`, `confirm`, `editor`,
|
|
378
|
+
* `select`, and `checkbox` each drive their own reducer. An open `select` is a suggestion list
|
|
379
|
+
* plus a typed line, because `open` means the answer need not come from the list.
|
|
376
380
|
* - **The binding projects through `matchesAnswer`.** Every answer is filled as
|
|
377
381
|
* `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the
|
|
378
382
|
* control's own shape, so a bare return on a field with no default binds as absence and the
|