@orkestrel/terminal 0.0.8 → 0.0.10

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.
@@ -1,10 +1,14 @@
1
- import { CheckboxOptions } from '@orkestrel/terminal';
2
- import { ConfirmOptions } from '@orkestrel/terminal';
3
- import { EditorOptions } from '@orkestrel/terminal';
4
- import { InputOptions } from '@orkestrel/terminal';
5
- import { PasswordOptions } from '@orkestrel/terminal';
6
- import { PromptFormInterface } from '@orkestrel/terminal';
7
- import { SelectOptions } from '@orkestrel/terminal';
1
+ import { FieldChoice } from '@orkestrel/form';
2
+ import { FieldControl } from '@orkestrel/form';
3
+ import { FieldValue } from '@orkestrel/form';
4
+ import { FormField } from '@orkestrel/form';
5
+ import { FormInterface } from '@orkestrel/form';
6
+ import { FormValues } from '@orkestrel/form';
7
+ import { PromptTheme } from '@orkestrel/terminal';
8
+ import { PromptThemeOptions } from '@orkestrel/terminal';
9
+ import { StylerInterface } from '@orkestrel/console';
10
+ import { TerminalInterface } from '@orkestrel/terminal';
11
+ import { TextField } from '@orkestrel/form';
8
12
 
9
13
  /** A carriage return (`\r`, U+000D) — returns the cursor to column 0 so a redraw starts at the line's left edge. */
10
14
  export declare const CARRIAGE_RETURN: string;
@@ -17,39 +21,55 @@ export declare const CARRIAGE_RETURN: string;
17
21
  export declare const CLEAR_DOWN: string;
18
22
 
19
23
  /**
20
- * Create the interactive terminal prompt {@link TerminalInterface} (T-c) — the local-TTY arm of the
21
- * prompt tri-surface, the env-symmetric sibling of the core headless `createPrompt` broker and the
22
- * SSE `createPromptClient` bridge. A `Terminal` answers each prompt LOCALLY at this machine's
23
- * keyboard: it reads raw-mode stdin, drives the pure core `*Reduce` reducers, renders each `view` in
24
- * place, and resolves on submit / rejects with a {@link import('@src/core').TerminalError} (`CANCEL`)
25
- * on ctrl-c. It is the ONLY impure part of the prompt stack.
24
+ * The format cue appended to a field's label for each control the walk reads as a line of text —
25
+ * the terminal has no date picker, no color well, and no file chooser, so the accepted shape is
26
+ * stated instead. A control with no entry needs none: `text` and `editor` accept any line,
27
+ * `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by
28
+ * format. The form's own rules still decide whether the typed value is acceptable.
29
+ */
30
+ export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string>>>;
31
+
32
+ /**
33
+ * Create the interactive terminal form driver — the local-keyboard arm of the terminal trio, beside
34
+ * the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the broker
35
+ * PARKS a live form until somebody elsewhere answers it, a `Terminal` answers one HERE: it walks the
36
+ * form's fields in schema order, drives each control's pure reducer over raw-mode stdin, binds every
37
+ * answer through the form's own `fill`, and submits. It is the only impure part of the terminal
38
+ * stack.
26
39
  *
27
40
  * @param options - See {@link TerminalOptions}
28
- * @returns A {@link TerminalInterface} — the six async prompt forms (`input` / `password` / `confirm`
29
- * / `select` / `checkbox` / `editor`) driven over the resolved streams
41
+ * @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams
30
42
  *
31
43
  * @remarks
32
- * - **Drives the pure reducers.** Each prompt builds its initial state (`create*State`), enters raw
33
- * mode ONCE, and feeds each keypress through `parseKey` → the reducer → an in-place re-render; the
34
- * driver owns no prompt logic itself (state / view / validation / cancel all come from the core).
35
- * - **Raw-mode leak-free.** Raw mode is entered exactly once per prompt and ALWAYS cleaned up — on
36
- * submit, on cancel, and on a throw — leaving no raw mode and no leaked `'data'` listener.
37
- * - **Injectable + guard-narrowed.** `options.input` / `options.output` default to `process.stdin` /
38
- * `process.stdout` but accept ANY {@link import('./types.js').InputStreamInterface} /
39
- * {@link import('./types.js').OutputStreamInterface}, resolved through their §14 guards (never an
40
- * `as`), so a test drives every prompt with a fake TTY that emits scripted key chunks and records
41
- * the rendered output — and asserts the resolved value, cancel-on-ctrl-c, and no leaked raw mode.
42
- * - **Non-TTY fallback.** When `input` is not a TTY (piped), raw mode is unavailable, so the prompts
43
- * fall back to `node:readline` line input (still validating); `select` / `checkbox` present a
44
- * numbered list, and `editor` reads lines until EOF.
44
+ * - **The form is the unit.** `ask` takes the caller's live form and returns its settled values. The
45
+ * returned promise is that form's own `answer`, so a caller holding the form can await either one.
46
+ * - **Every control renders.** Seven controls are read as a line of text with their format cue,
47
+ * `password` masks, `confirm` takes a key, `editor` takes a block, `select` and `checkbox` drive a
48
+ * list, and an open `select` accepts a value its list does not offer.
49
+ * - **The form decides.** A blank answer binds as absence, the form evaluates, and a refusal
50
+ * re-walks only the fields the walk can still edit.
51
+ * - **Injectable and guard-narrowed.** `input` and `output` default to `process.stdin` and
52
+ * `process.stdout` but accept any stream of the declared minimal shape, resolved through their
53
+ * guards rather than an assertion, so a test drives a whole form with a fake TTY that emits
54
+ * scripted key chunks and records every rendered byte.
55
+ * - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over
56
+ * `node:readline` line input.
45
57
  *
46
58
  * @example
47
59
  * ```ts
48
- * import { createTerminal } from '@src/server'
60
+ * import { createForm } from '@orkestrel/form'
61
+ * import { createTerminal } from '@orkestrel/terminal/server'
49
62
  *
50
63
  * const terminal = createTerminal()
51
- * const name = await terminal.input({ message: 'Your name' })
52
- * const proceed = await terminal.confirm({ message: 'Continue?', default: true })
64
+ * const values = await terminal.ask(
65
+ * createForm({
66
+ * label: 'Sign up',
67
+ * fields: [
68
+ * { control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },
69
+ * { control: 'confirm', name: 'terms', label: 'I accept the terms' },
70
+ * ],
71
+ * }),
72
+ * )
53
73
  * ```
54
74
  */
55
75
  export declare function createTerminal(options?: TerminalOptions): TerminalInterface;
@@ -59,8 +79,8 @@ export declare const CSI: string;
59
79
 
60
80
  /**
61
81
  * The cursor-UP sequence TEMPLATE (`ESC[{count}A`) — {@link import('./helpers.js').moveUp}
62
- * interpolates `{count}` with the number of lines to climb (the `{count}` placeholder idiom the core
63
- * terminals' `RULE_MESSAGES` uses). Kept as a template so the count stays out of the constant.
82
+ * interpolates the `{count}` placeholder with the number of lines to climb. Kept as a template so
83
+ * the count stays out of the constant.
64
84
  */
65
85
  export declare const CSI_UP: string;
66
86
 
@@ -73,42 +93,88 @@ export declare const CURSOR_HIDE: string;
73
93
  /** Show the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */
74
94
  export declare const CURSOR_SHOW: string;
75
95
 
96
+ /**
97
+ * The choices a `select` or `checkbox` field SHOWS but refuses — the complement of
98
+ * {@link enabledChoices}, rendered by {@link unavailableLine} above the list so a reader sees why a
99
+ * declared choice is missing from it.
100
+ *
101
+ * @param choices - The field's declared choices
102
+ * @returns The refused choices, in declared order
103
+ */
104
+ export declare function disabledChoices(choices: readonly FieldChoice[]): readonly FieldChoice[];
105
+
106
+ /**
107
+ * The choices a `select` or `checkbox` field actually OFFERS — the form refuses a disabled choice's
108
+ * value at every door, including a fill, so the walk never puts one in front of the cursor. Pair
109
+ * with {@link disabledChoices} to tell the reader what was withheld.
110
+ *
111
+ * @param choices - The field's declared choices
112
+ * @returns The choices the walk offers, in declared order
113
+ */
114
+ export declare function enabledChoices(choices: readonly FieldChoice[]): readonly FieldChoice[];
115
+
76
116
  /** The Escape byte (ESC, U+001B) — the lead byte of every CSI cursor-control sequence below. */
77
117
  export declare const ESCAPE: string;
78
118
 
79
119
  /** The comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */
80
120
  export declare const FALLBACK_CHECKBOX_HINT = "Enter numbers separated by commas";
81
121
 
122
+ /** The hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */
123
+ export declare const FALLBACK_CONFIRM_HINT = "(y/n)";
124
+
125
+ /** The hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */
126
+ export declare const FALLBACK_EDITOR_HINT = "(end of input finishes)";
127
+
82
128
  /**
83
- * The numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` / `checkbox`
84
- * fallback appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are
85
- * printed numbered and the user types the number(s) on a single readline line.
129
+ * The numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback
130
+ * appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are
131
+ * printed numbered and the user types one number on a single readline line.
86
132
  */
87
133
  export declare const FALLBACK_SELECT_HINT = "Enter a number";
88
134
 
89
135
  /**
90
- * The minimal input-stream shape the {@link TerminalInterface} reads — exactly the slice of a Node
91
- * `tty.ReadStream` / `process.stdin` it touches, and no more (AGENTS §21 — minimal interface). A
92
- * {@link TerminalOptions} `input` is narrowed to this via
93
- * {@link import('./helpers.js').isInputStream} (AGENTS §14 — narrow the boundary, never `as`), so a
94
- * test drives every prompt with a hand-built fake stream that emits scripted key chunks and never
95
- * touches the real `process.stdin` — and asserts raw mode is entered exactly once and always
96
- * cleaned up (no leak).
136
+ * Project any field the walk reads as a LINE OF TEXT into the {@link TextField} the text reducer
137
+ * takes — `text` itself, and the six controls a terminal has no widget for: `number`, `date`,
138
+ * `time`, `datetime`, `color`, and one `file` entry. The label carries that control's format cue
139
+ * from {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
140
+ * projection carries no rule, because the AUTHORITATIVE form still evaluates the answer this line
141
+ * binds; it exists only so one reducer can render seven controls.
142
+ *
143
+ * @param field - The field being read
144
+ * @returns The text field the reducer renders for it
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * fieldToText({ control: 'date', name: 'born', label: 'Birthday' })
149
+ * // { control: 'text', name: 'born', label: 'Birthday (YYYY-MM-DD)' }
150
+ * ```
151
+ */
152
+ export declare function fieldToText(field: FormField): TextField;
153
+
154
+ /** The instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
155
+ export declare const FILE_HINT = "One path per line, blank to finish";
156
+
157
+ /** The section header the walk writes when it enters a new field group, painted by the `message` role. */
158
+ export declare function groupHeader(styler: StylerInterface, theme: PromptTheme, label: string): string;
159
+
160
+ /**
161
+ * The minimal input-stream shape the driver reads — exactly the slice of a Node `tty.ReadStream` /
162
+ * `process.stdin` it touches, and no more. A {@link TerminalOptions} `input` is narrowed to this
163
+ * through {@link import('./helpers.js').isInputStream}, never an assertion, so a test drives a whole
164
+ * form with a hand-built fake stream that emits scripted key chunks, never touches the real
165
+ * `process.stdin`, and asserts that raw mode is entered once and always cleaned up.
97
166
  *
98
167
  * @remarks
99
168
  * - `on(event, listener)` / `off(event, listener)` — subscribe / unsubscribe a `'data'` chunk
100
- * listener (the irreducible event seam; a `Buffer` / string / `Uint8Array` chunk arrives). The two
101
- * required methods.
102
- * - `setRawMode(mode)` — switch the TTY in / out of raw mode (each keypress delivered immediately,
103
- * no line buffering, no echo). Present on a real `tty.ReadStream`; ABSENT on a piped, non-TTY
104
- * stream — its absence (or `isTTY !== true`) selects the {@link import('node:readline').Interface}
105
- * line-input fallback. The ONLY place raw mode is touched is {@link import('./Terminal.js').Terminal}'s
106
- * `#enterRaw`.
107
- * - `resume()` / `pause()` — start / stop the flow of `'data'` events. The raw-mode primitive
108
- * `resume()`s on enter and `pause()`s on cleanup; optional (a fake may omit them).
109
- * - `isTTY` — `true` on a real terminal, absent / `false` when piped to a file or another process.
110
- * The driver reads it to choose the raw-mode path (interactive arrow-key prompts) vs. the readline
111
- * fallback (numbered / line input).
169
+ * listener (the irreducible event seam; a `Buffer`, string, or `Uint8Array` chunk arrives). The
170
+ * two required methods.
171
+ * - `setRawMode(mode)` — switch the TTY in and out of raw mode (each keypress delivered
172
+ * immediately, no line buffering, no echo). Present on a real `tty.ReadStream`; ABSENT on a
173
+ * piped, non-TTY stream, and its absence (or `isTTY !== true`) selects the
174
+ * {@link import('node:readline').Interface} line-input fallback.
175
+ * - `resume()` / `pause()` — start / stop the flow of `'data'` events. Raw mode `resume()`s on
176
+ * enter and `pause()`s on cleanup; both are optional, so a fake may omit them.
177
+ * - `isTTY` — `true` on a real terminal, absent or `false` when piped to a file or another process.
112
178
  */
113
179
  export declare interface InputStreamInterface {
114
180
  on(event: 'data', listener: (chunk: string | Uint8Array) => void): void;
@@ -160,17 +226,6 @@ export declare function isOutputStream(value: unknown): value is OutputStreamInt
160
226
  */
161
227
  export declare function isReadable(value: unknown): value is NodeJS.ReadableStream;
162
228
 
163
- /**
164
- * Whether `value` is a Node {@link NodeJS.WritableStream} — a total structural guard (AGENTS §14)
165
- * checking for the callable `write` / `end` that `node:readline`'s `createInterface` accepts as its
166
- * `output`. Paired with {@link isReadable} so the non-TTY fallback narrows BOTH streams to the
167
- * readline boundary without an `as`. Never throws.
168
- *
169
- * @param value - The resolved output stream (or any value crossing the boundary)
170
- * @returns `true` when `value` has the writable methods readline needs
171
- */
172
- export declare function isWritable(value: unknown): value is NodeJS.WritableStream;
173
-
174
229
  /** A line feed (`\n`, U+000A) — the line terminator the driver writes after the final committed prompt view. */
175
230
  export declare const LINE_FEED: string;
176
231
 
@@ -185,6 +240,22 @@ export declare const LINE_FEED: string;
185
240
  */
186
241
  export declare function lineCount(view: string): number;
187
242
 
243
+ /** The mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
244
+ export declare const LOCKED_MARK = "(locked)";
245
+
246
+ /**
247
+ * The read-only line a LOCKED field renders — its label, the {@link LOCKED_MARK}, and the answer the
248
+ * form already holds. The walk writes this instead of a prompt, because the field is still
249
+ * validated and still submitted but must not be edited here.
250
+ *
251
+ * @param styler - The console styler that renders each role
252
+ * @param theme - The resolved prompt theme
253
+ * @param label - The field's label
254
+ * @param value - The held answer, from {@link valueToText}
255
+ * @returns The rendered line, with no trailing space when there is nothing to show
256
+ */
257
+ export declare function lockedLine(styler: StylerInterface, theme: PromptTheme, label: string, value: string): string;
258
+
188
259
  /**
189
260
  * The cursor-UP control sequence that moves the cursor up `count` lines (`ESC[{count}A`) — or the
190
261
  * empty string when `count` is zero or negative (no movement needed, and `ESC[0A` is a wasted write).
@@ -197,19 +268,29 @@ export declare function lineCount(view: string): number;
197
268
  export declare function moveUp(count: number): string;
198
269
 
199
270
  /**
200
- * The minimal output-stream shape the {@link TerminalInterface} writes — exactly the slice of a Node
201
- * `tty.WriteStream` / `process.stdout` it touches (AGENTS §21). A {@link TerminalOptions} `output`
202
- * is narrowed to this via {@link import('./helpers.js').isOutputStream} (AGENTS §14), so a test
203
- * records every byte the driver renders (the prompt view, the cursor-management sequences) with a
204
- * fake stream and asserts the rendered content (ANSI stripped) and the resolved value.
271
+ * The numbered choice list the non-TTY fallback prints — a piped stream cannot navigate with arrow
272
+ * keys, so each offered choice is printed with the number the reader types back. One line per
273
+ * choice, with no trailing newline.
274
+ *
275
+ * @param styler - The console styler that renders each role
276
+ * @param theme - The resolved prompt theme
277
+ * @param choices - The choices the walk offers, from {@link enabledChoices}
278
+ * @returns The rendered list
279
+ */
280
+ export declare function numberedList(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
281
+
282
+ /**
283
+ * The minimal output-stream shape the driver writes — exactly the slice of a Node `tty.WriteStream`
284
+ * / `process.stdout` it touches. A {@link TerminalOptions} `output` is narrowed to this through
285
+ * {@link import('./helpers.js').isOutputStream}, never an assertion, so a test records every byte
286
+ * the walk renders and asserts the rendered content with the ANSI stripped.
205
287
  *
206
288
  * @remarks
207
- * - `write(text)` — the one required method: push a chunk (a rendered prompt view, a cursor / clear
208
- * escape sequence) to the stream. A real stream returns a backpressure boolean; the driver ignores
209
- * the return (a prompt is human-paced, never backpressured), so a fake may return `void`.
210
- * - `isTTY` — present and `true` on a real terminal. The driver does not branch its rendering on it
211
- * (the styler already decided color); it is part of the minimal shape for symmetry with the input
212
- * stream and so a consumer may inspect it.
289
+ * - `write(text)` — the one required method: push a chunk (a rendered field view, a group header, a
290
+ * cursor or clear sequence) to the stream. A real stream returns a backpressure boolean; the
291
+ * driver ignores the return, because a prompt is human-paced, so a fake may return `void`.
292
+ * - `isTTY` — present and `true` on a real terminal. The driver does not branch its rendering on
293
+ * it; the styler already decided color.
213
294
  */
214
295
  export declare interface OutputStreamInterface {
215
296
  write(text: string): boolean | void;
@@ -248,74 +329,96 @@ export declare function rawCapable(input: InputStreamInterface): boolean;
248
329
  export declare function redrawPrefix(previousLines: number): string;
249
330
 
250
331
  /**
251
- * The interactive terminal prompt driver (T-c) — the third {@link TerminalInterface} surface (beside
252
- * the core headless `Prompt` broker and the SSE `PromptClient` bridge), and the ONLY impure part of
253
- * the prompt stack. It reads the TTY and DRIVES the pure core `*Reduce` reducers: it feeds raw-mode
254
- * stdin bytes through `parseKey` into the matching reducer, renders the returned `view` in place, and
255
- * resolves on a `submit` step / rejects with a {@link TerminalError} (`CANCEL`) on ctrl-c. It owns no
256
- * prompt logic of its own — state, view, validation, and the cancel signal all come from the pure
257
- * core; this class owns ONLY the cursor + raw-mode + in-place re-render mechanics.
258
- *
259
- * @remarks
260
- * See {@link TerminalInterface} for the behavioral contract (raw-mode leak-freedom, the in-place
261
- * re-render, the non-TTY readline fallback, event-free).
332
+ * What a field is told when the walk read an answer the control cannot hold — a word typed into a
333
+ * `number`, an off-list value typed into an open `select` whose choice is refused. The value binds
334
+ * as absence and this message is invalidated onto the field, so the walk re-asks it with the reason
335
+ * on screen.
262
336
  */
263
- export declare class Terminal implements TerminalInterface {
264
- #private;
265
- constructor(options?: TerminalOptions);
266
- input(options: InputOptions): Promise<string>;
267
- password(options: PasswordOptions): Promise<string>;
268
- confirm(options: ConfirmOptions): Promise<boolean>;
269
- select(options: SelectOptions): Promise<string>;
270
- checkbox(options: CheckboxOptions): Promise<readonly string[]>;
271
- editor(options: EditorOptions): Promise<string>;
272
- }
337
+ export declare const REFUSAL_MESSAGE = "Enter a value this field accepts";
338
+
339
+ /** The lead on the line listing an open `select`'s offered values, which a typed answer may ignore. */
340
+ export declare const SUGGESTION_LEAD = "Suggestions";
341
+
342
+ /** The line listing an OPEN select's offered values above its text prompt — a suggestion list, because an open select admits an answer the list does not offer. */
343
+ export declare function suggestionLine(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
273
344
 
274
345
  /**
275
- * The interactive terminal prompt DRIVER (the third {@link PromptFormInterface} surface) — reads the
276
- * TTY and DRIVES the pure core `*Reduce` reducers, the ONLY impure part of the prompt stack. Where
277
- * the core `Prompt` broker PARKS each prompt and the `PromptClient` bridges one over SSE, a
278
- * `Terminal` answers each prompt LOCALLY at this machine's keyboard: it feeds raw-mode stdin bytes
279
- * through `parseKey` into the matching reducer, renders the returned `view` in place (tracking the
280
- * previous view's line count to overwrite it), and resolves on a `submit` step / rejects with a
281
- * {@link import('@src/core').TerminalError} (`CANCEL`) on ctrl-c.
346
+ * The interactive terminal form DRIVER — the {@link TerminalInterface} implementation for a human at
347
+ * this machine's keyboard, and the only impure part of the terminal stack. {@link ask} walks one
348
+ * form's fields in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching
349
+ * pure reducer, renders each returned view in place, and binds every answer through the form's own
350
+ * `fill`. It owns no form logic: the schema, the rules, the values, and the settlement all belong to
351
+ * the form it is given, and this class owns only raw mode, the cursor, and the re-render.
282
352
  *
283
353
  * @remarks
284
- * - **Drives the pure reducers.** Each method builds the initial state (`create*State(options)`),
285
- * enters raw mode once, and on each keypress runs `parseKey` → the reducer → an in-place re-render;
286
- * it owns NO prompt logic of its own (state, view, validation, and the cancel signal all come from
287
- * the pure core).
288
- * - **Raw-mode leak-free.** Raw mode is entered exactly once per prompt and ALWAYS cleaned up — on
289
- * submit, on cancel, and on a throw — leaving no raw mode and no leaked `'data'` listener.
290
- * - **In-place re-render.** Between keystrokes the cursor is moved up over the previous view's lines,
291
- * the screen cleared down, and the new view written — so a `select` / `checkbox` list redraws live;
292
- * the cursor is hidden during the prompt and restored after.
293
- * - **Non-TTY fallback.** When `input` is not a TTY, raw mode is unavailable, so the prompts fall
294
- * back to `node:readline` line input (still validating) — `select` / `checkbox` present a numbered
295
- * list read via a readline line, and `editor` reads lines until EOF.
296
- * - **Event-free.** A `Terminal` is a request / response driver — each prompt is a Promise; there is
297
- * no observable lifecycle worth an emitter, so (unlike the broker / bridge) it carries none.
354
+ * See {@link TerminalInterface} for the driving contract. The walk itself:
355
+ *
356
+ * - **Twelve controls, seven reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, and
357
+ * each `file` entry are read as one line of text through {@link fieldToText}, which appends that
358
+ * control's format cue to the label. `password`, `confirm`, `editor`, `select`, and `checkbox`
359
+ * each drive their own reducer. An open `select` is a suggestion list plus a typed line, because
360
+ * `open` means the answer need not come from the list.
361
+ * - **The binding projects through `matchesAnswer`.** Every answer is filled as
362
+ * `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the
363
+ * control's own shape, so a bare return on a field with no default binds as ABSENCE and the
364
+ * form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and
365
+ * invalidates the field, so the walk asks again with the reason on screen.
366
+ * - **Visibility is honored.** A `hidden` field and a field currently in `form.disabled` are
367
+ * skipped; a `locked` field renders read-only; entering a new group writes its label as a section
368
+ * header.
369
+ * - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring
370
+ * fields the walk can edit and submits again. When every erroring field is one the walk cannot
371
+ * edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could
372
+ * not change the answer.
373
+ * - **Raw-mode leak-free.** Raw mode is entered once per field and always cleaned up: on submit, on
374
+ * cancel, on a throw, and when the form is abandoned under an active read.
375
+ * - **Non-TTY fallback.** When `input` is not a TTY, the same walk runs over `node:readline` line
376
+ * input: `select` and `checkbox` print a numbered list, and `editor` reads to end of input.
298
377
  */
299
- export declare interface TerminalInterface extends PromptFormInterface {
378
+ export declare class Terminal implements TerminalInterface {
379
+ #private;
380
+ constructor(options?: TerminalOptions);
381
+ ask(form: FormInterface): Promise<FormValues>;
300
382
  }
301
383
 
302
384
  /**
303
- * Options for {@link import('./factories.js').createTerminal} — both streams optional, so a bare
304
- * `createTerminal()` drives the real `process.stdin` / `process.stdout`.
385
+ * Options for {@link import('./factories.js').createTerminal} — every member optional, so a bare
386
+ * `createTerminal()` walks a form over the real `process.stdin` / `process.stdout` with the default
387
+ * theme.
305
388
  *
306
389
  * @remarks
307
390
  * - `input` — the stream keystrokes are read from; defaults to `process.stdin`. Any
308
- * {@link InputStreamInterface}-shaped stream is accepted (resolved through
309
- * {@link import('./helpers.js').isInputStream}, never `as`), so a test injects a fake TTY that
310
- * emits scripted `'data'` chunks. When the stream is not a TTY (no `setRawMode` / `isTTY !== true`),
311
- * the prompts fall back to `node:readline` line input.
312
- * - `output` — the stream the prompt view is rendered to; defaults to `process.stdout`. Any
313
- * {@link OutputStreamInterface}-shaped stream is accepted (resolved through
314
- * {@link import('./helpers.js').isOutputStream}), so a test records the rendered output.
391
+ * {@link InputStreamInterface}-shaped stream is accepted, resolved through
392
+ * {@link import('./helpers.js').isInputStream}, so a test injects a fake TTY that emits scripted
393
+ * `'data'` chunks. A stream that is not a TTY falls back to `node:readline` line input.
394
+ * - `output` — the stream each view is rendered to; defaults to `process.stdout`. Any
395
+ * {@link OutputStreamInterface}-shaped stream is accepted, resolved through
396
+ * {@link import('./helpers.js').isOutputStream}, so a test records the rendered output.
397
+ * - `theme` — the glyphs and role styles every rendered line is painted with, merged over
398
+ * {@link import('@src/core').DEFAULT_PROMPT_THEME} leaf by leaf by
399
+ * {@link import('@src/core').createPromptTheme}. Supplying one icon or one role leaves every
400
+ * other slot at its default.
315
401
  */
316
402
  export declare interface TerminalOptions {
317
403
  readonly input?: InputStreamInterface;
318
404
  readonly output?: OutputStreamInterface;
405
+ readonly theme?: PromptThemeOptions;
319
406
  }
320
407
 
408
+ /** The lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a reader sees why one is missing from the list below. */
409
+ export declare const UNAVAILABLE_LEAD = "Unavailable";
410
+
411
+ /** The line naming the choices a field shows but refuses, written above the list the walk drives. */
412
+ export declare function unavailableLine(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
413
+
414
+ /**
415
+ * Project one held answer into the text a read-only line shows — a scalar as itself, a boolean as
416
+ * `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders
417
+ * as nothing, because a locked field nobody has answered has nothing to show.
418
+ *
419
+ * @param value - The answer the form holds for a field, or absence
420
+ * @returns The text to render for it
421
+ */
422
+ export declare function valueToText(value: FieldValue | undefined): string;
423
+
321
424
  export { }