@orkestrel/terminal 0.0.13 → 0.0.15

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,27 +1,25 @@
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';
12
-
13
- /** A carriage return (`\r`, U+000D) — returns the cursor to column 0 so a redraw starts at the line's left edge. */
14
- export declare const CARRIAGE_RETURN: string;
1
+ import type { FieldChoice } from '@orkestrel/form';
2
+ import type { FieldControl } from '@orkestrel/form';
3
+ import type { FieldValue } from '@orkestrel/form';
4
+ import type { FormField } from '@orkestrel/form';
5
+ import type { FormInterface } from '@orkestrel/form';
6
+ import type { FormValues } from '@orkestrel/form';
7
+ import type { PromptTheme } from '@orkestrel/terminal';
8
+ import type { PromptThemeOptions } from '@orkestrel/terminal';
9
+ import type { StreamTargetInterface } from '@orkestrel/console/server';
10
+ import type { StylerInterface } from '@orkestrel/console';
11
+ import type { TerminalInterface } from '@orkestrel/terminal';
12
+ import type { TextField } from '@orkestrel/form';
15
13
 
16
14
  /**
17
- * Erase from the cursor down to the end of the screen (`ESC[J`) — wipes the WHOLE previous (possibly
18
- * multi-line `select` / `checkbox`) view in one write before the new view is rendered, so a redraw
19
- * never leaves orphaned rows below.
15
+ * Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the whole previous view,
16
+ * which a `select` or `checkbox` can spread over several lines, in one write before the new view is
17
+ * rendered, so a redraw never leaves orphaned rows behind.
20
18
  */
21
19
  export declare const CLEAR_DOWN: string;
22
20
 
23
21
  /**
24
- * The format cue appended to a field's label for each control the walk reads as a line of text —
22
+ * Holds the format cue appended to a field's label for each control the walk reads as a line of text —
25
23
  * the terminal has no date picker, no color well, and no file chooser, so the accepted shape is
26
24
  * stated instead. A control with no entry needs none: `text` and `editor` accept any line,
27
25
  * `password` masks one, and `confirm`, `select`, and `checkbox` are answered by key rather than by
@@ -30,12 +28,12 @@ export declare const CLEAR_DOWN: string;
30
28
  export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string>>>;
31
29
 
32
30
  /**
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.
31
+ * Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio,
32
+ * beside the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the
33
+ * broker parks a live form until somebody elsewhere answers it, a `Terminal` answers one here: it
34
+ * walks the form's fields in schema order, drives each control's pure reducer over raw-mode stdin,
35
+ * binds every answer through the form's own `fill`, and submits. It is the only impure part of the
36
+ * terminal stack.
39
37
  *
40
38
  * @param options - See {@link TerminalOptions}
41
39
  * @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams
@@ -43,7 +41,7 @@ export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string
43
41
  * @remarks
44
42
  * - **The form is the unit.** `ask` takes the caller's live form and returns its settled values. The
45
43
  * 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,
44
+ * - **Every control renders.** The line-read controls are read as a line of text with their format cue,
47
45
  * `password` masks, `confirm` takes a key, `editor` takes a block, `select` and `checkbox` drive a
48
46
  * list, and an open `select` accepts a value its list does not offer.
49
47
  * - **The form decides.** A blank answer binds as absence, the form evaluates, and a refusal
@@ -55,90 +53,82 @@ export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string
55
53
  * - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over
56
54
  * `node:readline` line input.
57
55
  *
58
- * @example
56
+ * @example Ask one form at this keyboard
59
57
  * ```ts
60
58
  * import { createForm } from '@orkestrel/form'
59
+ * import { isTerminalError } from '@orkestrel/terminal'
61
60
  * import { createTerminal } from '@orkestrel/terminal/server'
62
61
  *
63
- * const terminal = createTerminal()
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
- * )
62
+ * const terminal = createTerminal() // process.stdin / process.stdout by default
63
+ * const form = createForm({
64
+ * label: 'Sign up',
65
+ * fields: [
66
+ * { control: 'text', name: 'name', label: 'Your name', rule: { required: true, minimum: 2 } },
67
+ * { control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },
68
+ * { control: 'password', name: 'token', label: 'Token' },
69
+ * { control: 'confirm', name: 'terms', label: 'Accept the terms', rule: { required: true } },
70
+ * {
71
+ * control: 'select',
72
+ * name: 'role',
73
+ * label: 'Role',
74
+ * choices: [
75
+ * { value: 'admin', label: 'Admin' },
76
+ * { value: 'viewer', label: 'Viewer', help: 'read-only' },
77
+ * ],
78
+ * },
79
+ * ],
80
+ * })
81
+ *
82
+ * try {
83
+ * const values = await terminal.ask(form)
84
+ * deploy(values)
85
+ * } catch (error) {
86
+ * // Ctrl-c: the walk ended, and the form is still `editing` for whoever owns it.
87
+ * if (isTerminalError(error) && error.code === 'CANCEL') form.destroy()
88
+ * }
73
89
  * ```
74
90
  */
75
91
  export declare function createTerminal(options?: TerminalOptions): TerminalInterface;
76
92
 
77
- /** The Control Sequence Introducer (`ESC[`) — the prefix of every cursor / erase sequence. */
78
- export declare const CSI: string;
79
-
80
93
  /**
81
- * The cursor-UP sequence TEMPLATE (`ESC[{count}A`) — {@link import('./helpers.js').moveUp}
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.
94
+ * Holds the cursor-up sequence template (`ESC[{count}A`) —
95
+ * {@link import('./helpers.js').renderCursorUp} interpolates the `{count}` placeholder with the
96
+ * number of lines to climb. Kept as a template so the count stays out of the constant.
84
97
  */
85
98
  export declare const CSI_UP: string;
86
99
 
87
100
  /**
88
- * Hide the cursor (`ESC[?25l`) — written before the driver starts redrawing a prompt so the cursor
101
+ * Hides the cursor (`ESC[?25l`) — written before the driver starts redrawing a prompt so the cursor
89
102
  * does not flicker across the view during an in-place re-render; paired with {@link CURSOR_SHOW}.
90
103
  */
91
104
  export declare const CURSOR_HIDE: string;
92
105
 
93
- /** Show the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */
106
+ /** Shows the cursor (`ESC[?25h`) — restores the cursor after a prompt resolves / cancels (the {@link CURSOR_HIDE} pair). */
94
107
  export declare const CURSOR_SHOW: string;
95
108
 
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
-
116
- /** The Escape byte (ESC, U+001B) — the lead byte of every CSI cursor-control sequence below. */
117
- export declare const ESCAPE: string;
118
-
119
- /** The comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */
109
+ /** Holds the comma-separated multi-select hint the non-TTY `checkbox` fallback shows (the user types one or more numbers). */
120
110
  export declare const FALLBACK_CHECKBOX_HINT = "Enter numbers separated by commas";
121
111
 
122
- /** The hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */
112
+ /** Holds the hint the non-TTY `confirm` fallback shows — a piped stream sends a whole line, so the answer is typed rather than pressed. */
123
113
  export declare const FALLBACK_CONFIRM_HINT = "(y/n)";
124
114
 
125
- /** The hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */
115
+ /** Holds the hint the non-TTY `editor` fallback shows — a piped stream has no ctrl-d, so end of input finishes the block. */
126
116
  export declare const FALLBACK_EDITOR_HINT = "(end of input finishes)";
127
117
 
128
118
  /**
129
- * The numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback
119
+ * Holds the numbered-list prompt the non-TTY {@link import('./Terminal.js').Terminal} `select` fallback
130
120
  * appends — a piped (non-terminal) stream cannot navigate with arrow keys, so the choices are
131
121
  * printed numbered and the user types one number on a single readline line.
132
122
  */
133
123
  export declare const FALLBACK_SELECT_HINT = "Enter a number";
134
124
 
135
125
  /**
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.
126
+ * Projects any field the walk reads as a line of text into the {@link TextField} the text reducer
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
129
+ * {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
130
+ * projection carries no rule, because the authoritative form still evaluates the answer this line
131
+ * binds; it exists only so one reducer covers every one of them.
142
132
  *
143
133
  * @param field - The field being read
144
134
  * @returns The text field the reducer renders for it
@@ -151,14 +141,31 @@ export declare const FALLBACK_SELECT_HINT = "Enter a number";
151
141
  */
152
142
  export declare function fieldToText(field: FormField): TextField;
153
143
 
154
- /** The instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
144
+ /** Holds the instruction a `file` field with `multiple` shows before its entries — one path per line, and a blank line ends the list. */
155
145
  export declare const FILE_HINT = "One path per line, blank to finish";
156
146
 
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;
147
+ /**
148
+ * Returns the choices a `select` or `checkbox` field shows but refuses — the complement of
149
+ * {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees
150
+ * why a declared choice is missing from it.
151
+ *
152
+ * @param choices - The field's declared choices
153
+ * @returns The refused choices, in declared order
154
+ */
155
+ export declare function filterDisabled(choices: readonly FieldChoice[]): readonly FieldChoice[];
156
+
157
+ /**
158
+ * Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled
159
+ * choice's value at every door, including a fill, so the walk never puts one in front of the
160
+ * cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.
161
+ *
162
+ * @param choices - The field's declared choices
163
+ * @returns The choices the walk offers, in declared order
164
+ */
165
+ export declare function filterEnabled(choices: readonly FieldChoice[]): readonly FieldChoice[];
159
166
 
160
167
  /**
161
- * The minimal input-stream shape the driver reads — exactly the slice of a Node `tty.ReadStream` /
168
+ * Represents the minimal input-stream shape the driver reads — exactly the slice of a Node `tty.ReadStream` /
162
169
  * `process.stdin` it touches, and no more. A {@link TerminalOptions} `input` is narrowed to this
163
170
  * through {@link import('./helpers.js').isInputStream}, never an assertion, so a test drives a whole
164
171
  * form with a hand-built fake stream that emits scripted key chunks, never touches the real
@@ -167,7 +174,7 @@ export declare function groupHeader(styler: StylerInterface, theme: PromptTheme,
167
174
  * @remarks
168
175
  * - `on(event, listener)` / `off(event, listener)` — subscribe / unsubscribe a `'data'` chunk
169
176
  * listener (the irreducible event seam; a `Buffer`, string, or `Uint8Array` chunk arrives). The
170
- * two required methods.
177
+ * required methods.
171
178
  * - `setRawMode(mode)` — switch the TTY in and out of raw mode (each keypress delivered
172
179
  * immediately, no line buffering, no echo). Present on a real `tty.ReadStream`; ABSENT on a
173
180
  * piped, non-TTY stream, and its absence (or `isTTY !== true`) selects the
@@ -177,195 +184,202 @@ export declare function groupHeader(styler: StylerInterface, theme: PromptTheme,
177
184
  * - `isTTY` — `true` on a real terminal, absent or `false` when piped to a file or another process.
178
185
  */
179
186
  export declare interface InputStreamInterface {
187
+ /** Subscribes a `'data'` chunk listener — the irreducible event seam. */
180
188
  on(event: 'data', listener: (chunk: string | Uint8Array) => void): void;
189
+ /** Unsubscribes that listener. The driver always pairs it, so no listener leaks. */
181
190
  off(event: 'data', listener: (chunk: string | Uint8Array) => void): void;
191
+ /** Switches the TTY in and out of raw mode. Absent on a piped stream. */
182
192
  setRawMode?(mode: boolean): void;
193
+ /** Starts the flow of `'data'` events. */
183
194
  resume?(): void;
195
+ /** Stops it again on cleanup. */
184
196
  pause?(): void;
197
+ /** Reports whether the stream is a real terminal; absent or false when piped. */
185
198
  readonly isTTY?: boolean;
186
199
  }
187
200
 
188
201
  /**
189
- * Whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` / `off`
190
- * `'data'` subscription methods. A total type guard (AGENTS §14): it NEVER throws and returns `false`
191
- * for anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,
192
- * or a fake TTY a test injects) to the exact slice the driver reads — no `as`.
202
+ * Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` /
203
+ * `off` `'data'` subscription methods. A total type guard: it never throws and returns `false` for
204
+ * anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,
205
+ * or a fake TTY a test injects) to the exact slice the driver reads, never an assertion.
193
206
  *
194
207
  * @remarks
195
208
  * Only `on` / `off` are required (the irreducible event seam); `setRawMode` / `resume` / `pause` /
196
209
  * `isTTY` are optional on {@link InputStreamInterface}, so their absence does not disqualify a stream
197
- * — a piped, non-TTY stream is still a valid input, just one the driver reads through the readline
210
+ * — a piped, non-TTY stream is still a valid input, one the driver reads through the readline
198
211
  * fallback rather than raw mode.
199
212
  *
200
213
  * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
201
- * @returns `true` when `value` has callable `on` and `off`
214
+ * @returns True if `value` has callable `on` and `off`; false otherwise
202
215
  */
203
216
  export declare function isInputStream(value: unknown): value is InputStreamInterface;
204
217
 
205
218
  /**
206
- * Whether `value` is a usable {@link OutputStreamInterface} — a record with a callable `write`. A
207
- * total type guard (AGENTS §14): it NEVER throws and returns `false` for anything off-shape, so it
208
- * narrows the output boundary (the real `process.stdout`, or a recording fake a test injects) to the
209
- * one method the driver writes through — no `as`.
210
- *
211
- * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
212
- * @returns `true` when `value` has a callable `write`
213
- */
214
- export declare function isOutputStream(value: unknown): value is OutputStreamInterface;
215
-
216
- /**
217
- * Whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard (AGENTS §14)
218
- * checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface` requires
219
- * as its `input`. The non-TTY fallback narrows the resolved input stream through this before handing
220
- * it to readline (never an `as`), so a real piped `process.stdin` (or a `PassThrough` a test injects)
221
- * crosses into the readline boundary honestly. Never throws; returns `false` for a minimal fake that
222
- * isn't a full readable.
219
+ * Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard
220
+ * checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface`
221
+ * requires as its `input`. The non-TTY fallback narrows the resolved input stream through this
222
+ * before handing it to readline, never through an assertion, so a real piped `process.stdin` (or a
223
+ * `PassThrough` a test injects) crosses into the readline boundary honestly. Never throws; returns
224
+ * `false` for a minimal fake that isn't a full readable.
223
225
  *
224
226
  * @param value - The resolved input stream (or any value crossing the boundary)
225
- * @returns `true` when `value` has the readable methods readline needs
227
+ * @returns True if `value` has the readable methods readline needs; false otherwise
226
228
  */
227
229
  export declare function isReadable(value: unknown): value is NodeJS.ReadableStream;
228
230
 
229
- /** A line feed (`\n`, U+000A) — the line terminator the driver writes after the final committed prompt view. */
230
- export declare const LINE_FEED: string;
231
-
232
231
  /**
233
- * The number of terminal LINES a rendered prompt `view` occupies — one more than its newline count
234
- * (a view with no newline is a single line; N newlines span N+1 lines). The basis of the in-place
235
- * re-render: the driver records the line count of the view it just wrote so the next redraw knows how
236
- * far up to move the cursor before overwriting. Total; an empty string is one (empty) line.
232
+ * Counts the terminal lines a rendered prompt `view` occupies — one more than its newline count, so
233
+ * a view with no newline is a single line and a view with N newlines spans N+1 lines. The basis of
234
+ * the in-place re-render: the driver records the line count of the view it wrote so the next redraw
235
+ * knows how far up to move the cursor before overwriting. Total; an empty string is one empty line.
237
236
  *
238
237
  * @param view - The rendered (possibly multi-line, possibly ANSI-styled) view string
239
238
  * @returns The number of lines the view spans (always at least 1)
240
239
  */
241
240
  export declare function lineCount(view: string): number;
242
241
 
243
- /** The mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
242
+ /** Holds the mark on a locked field's line — the walk renders its value and moves on, because the form refuses an edit there. */
244
243
  export declare const LOCKED_MARK = "(locked)";
245
244
 
246
245
  /**
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.
246
+ * Returns the full reposition-and-clear prefix to write before re-rendering a prompt view in place
247
+ * — given the line count of the previous view, it moves the cursor up over those lines, returns it
248
+ * to column 0, and erases everything from there to the end of the screen, so the next view is drawn
249
+ * on a clean region and a taller previous view leaves no orphaned rows. Pure; the driver writes
250
+ * this immediately followed by the new view.
250
251
  *
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
252
+ * @remarks
253
+ * For the first render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so
254
+ * the prefix is a carriage return + clear-down — the prompt draws from the current line. For a
255
+ * subsequent render it climbs `previousLines - 1` lines (the cursor is on the last line of the prior
256
+ * view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable
257
+ * without a real terminal.
258
+ *
259
+ * @param previousLines - The line count of the view on screen (from {@link lineCount})
260
+ * @returns The control-sequence prefix to write before the new view
261
+ */
262
+ export declare function redrawPrefix(previousLines: number): string;
263
+
264
+ /**
265
+ * States what a field is told when the walk read an answer the control cannot hold — a word typed into a
266
+ * `number`, an off-list value typed into an open `select` whose choice is refused. The value binds
267
+ * as absence and this message is invalidated onto the field, so the walk re-asks it with the reason
268
+ * on screen.
256
269
  */
257
- export declare function lockedLine(styler: StylerInterface, theme: PromptTheme, label: string, value: string): string;
270
+ export declare const REFUSAL_MESSAGE = "Enter a value this field accepts";
258
271
 
259
272
  /**
260
- * The cursor-UP control sequence that moves the cursor up `count` lines (`ESC[{count}A`) — or the
261
- * empty string when `count` is zero or negative (no movement needed, and `ESC[0A` is a wasted write).
262
- * The pure step the in-place re-render uses to climb back over the previous view before clearing it.
263
- * Total.
273
+ * Returns the cursor-up control sequence that moves the cursor up `count` lines (`ESC[{count}A`),
274
+ * or the empty string when `count` is zero or negative, because no movement is needed and `ESC[0A`
275
+ * is a wasted write. The pure step the in-place re-render uses to climb back over the previous view
276
+ * before clearing it. Total.
264
277
  *
265
278
  * @param count - How many lines to move the cursor up
266
279
  * @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`
267
280
  */
268
- export declare function moveUp(count: number): string;
281
+ export declare function renderCursorUp(count: number): string;
269
282
 
270
283
  /**
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.
284
+ * Renders the section header the walk writes when it enters a new field group, painted by the
285
+ * `message` role.
274
286
  *
275
287
  * @param styler - The console styler that renders each role
276
288
  * @param theme - The resolved prompt theme
277
- * @param choices - The choices the walk offers, from {@link enabledChoices}
278
- * @returns The rendered list
289
+ * @param label - The group's declared label, falling back to its own name
290
+ * @returns The rendered section header
279
291
  */
280
- export declare function numberedList(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
292
+ export declare function renderGroupHeader(styler: StylerInterface, theme: PromptTheme, label: string): string;
281
293
 
282
294
  /**
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.
295
+ * Renders the read-only line a locked field shows — its label, the {@link LOCKED_MARK}, and the
296
+ * answer the form already holds. The walk writes this instead of a prompt, because the field is
297
+ * still validated and still submitted but must not be edited here.
287
298
  *
288
- * @remarks
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.
299
+ * @param styler - The console styler that renders each role
300
+ * @param theme - The resolved prompt theme
301
+ * @param label - The field's label
302
+ * @param value - The held answer, from {@link valueToText}
303
+ * @returns The rendered line, with no trailing space when there is nothing to show
294
304
  */
295
- export declare interface OutputStreamInterface {
296
- write(text: string): boolean | void;
297
- readonly isTTY?: boolean;
298
- }
305
+ export declare function renderLockedLine(styler: StylerInterface, theme: PromptTheme, label: string, value: string): string;
299
306
 
300
307
  /**
301
- * Whether an input stream can be driven in RAW mode — it both reports `isTTY === true` AND exposes a
302
- * callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to choose its path:
303
- * `true` ⇒ the interactive raw-mode prompts (arrow-key navigation, live re-render); `false` ⇒ the
304
- * `node:readline` line-input fallback (a piped / non-terminal stream cannot enter raw mode). Total —
305
- * never throws.
308
+ * Renders the numbered choice list the non-TTY fallback prints — a piped stream cannot navigate
309
+ * with arrow keys, so each offered choice is printed with the number the reader types back. One
310
+ * line per choice, with no trailing newline.
306
311
  *
307
- * @param input - The resolved {@link InputStreamInterface}
308
- * @returns `true` when the stream is a TTY with `setRawMode`
312
+ * @param styler - The console styler that renders each role
313
+ * @param theme - The resolved prompt theme
314
+ * @param choices - The choices the walk offers, from {@link filterEnabled}
315
+ * @returns The rendered list
309
316
  */
310
- export declare function rawCapable(input: InputStreamInterface): boolean;
317
+ export declare function renderNumberedList(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
311
318
 
312
319
  /**
313
- * The full reposition-and-clear prefix to write BEFORE re-rendering a prompt view in place — given
314
- * the line count of the PREVIOUS view, it moves the cursor up over those lines, returns it to column
315
- * 0, and erases everything from there to the end of the screen, so the next view is drawn on a clean
316
- * region (no orphaned rows from a taller previous view). Pure; the driver writes this immediately
317
- * followed by the new view.
318
- *
319
- * @remarks
320
- * For the FIRST render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so
321
- * the prefix is just a carriage return + clear-down — the prompt draws from the current line. For a
322
- * subsequent render it climbs `previousLines - 1` lines (the cursor is on the LAST line of the prior
323
- * view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable
324
- * without a real terminal.
320
+ * Renders the line listing an open select's offered values above its text prompt — a suggestion
321
+ * list, because an open select admits an answer the list does not offer.
325
322
  *
326
- * @param previousLines - The line count of the view currently on screen (from {@link lineCount})
327
- * @returns The control-sequence prefix to write before the new view
323
+ * @param styler - The console styler that renders each role
324
+ * @param theme - The resolved prompt theme
325
+ * @param choices - The choices the open select offers, from {@link filterEnabled}
326
+ * @returns The rendered suggestion line
328
327
  */
329
- export declare function redrawPrefix(previousLines: number): string;
328
+ export declare function renderSuggestionLine(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
330
329
 
331
330
  /**
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.
331
+ * Renders the line naming the choices a field shows but refuses, written above the list the walk
332
+ * drives.
333
+ *
334
+ * @param styler - The console styler that renders each role
335
+ * @param theme - The resolved prompt theme
336
+ * @param choices - The refused choices, from {@link filterDisabled}
337
+ * @returns The rendered unavailable line
336
338
  */
337
- export declare const REFUSAL_MESSAGE = "Enter a value this field accepts";
339
+ export declare function renderUnavailableLine(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
338
340
 
339
- /** The lead on the line listing an open `select`'s offered values, which a typed answer may ignore. */
341
+ /**
342
+ * Holds the lead on the line listing an open `select`'s offered values, which a typed answer can
343
+ * ignore.
344
+ */
340
345
  export declare const SUGGESTION_LEAD = "Suggestions";
341
346
 
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;
347
+ /**
348
+ * Checks whether an input stream can be driven in raw mode — it reports `isTTY === true` and
349
+ * exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to
350
+ * choose its path: true selects the interactive raw-mode fields, with arrow-key navigation and a
351
+ * live re-render; false selects the `node:readline` line-input fallback, because a piped or
352
+ * non-terminal stream cannot enter raw mode. Total — never throws.
353
+ *
354
+ * @param input - The resolved {@link InputStreamInterface}
355
+ * @returns True if the stream is a TTY with `setRawMode`; false otherwise
356
+ */
357
+ export declare function supportsRawMode(input: InputStreamInterface): boolean;
344
358
 
345
359
  /**
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.
360
+ * Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive
361
+ * form driver, and the only impure part of the terminal stack. {@link ask} walks one form's fields
362
+ * in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching pure reducer,
363
+ * renders each returned view in place, binds every answer through the form's own `fill`, and
364
+ * re-asks what the form refused. It owns no form logic: the schema, the rules, the values, and the
365
+ * settlement all belong to the form it is given, and this class owns only raw mode, the cursor, and
366
+ * the re-render.
352
367
  *
353
368
  * @remarks
354
369
  * See {@link TerminalInterface} for the driving contract. The walk itself:
355
370
  *
356
- * - **Twelve controls, seven reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, and
371
+ * - **Controls and reducers.** `text`, `number`, `date`, `time`, `datetime`, `color`, and
357
372
  * each `file` entry are read as one line of text through {@link fieldToText}, which appends that
358
373
  * control's format cue to the label. `password`, `confirm`, `editor`, `select`, and `checkbox`
359
374
  * each drive their own reducer. An open `select` is a suggestion list plus a typed line, because
360
375
  * `open` means the answer need not come from the list.
361
376
  * - **The binding projects through `matchesAnswer`.** Every answer is filled as
362
377
  * `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
378
+ * control's own shape, so a bare return on a field with no default binds as absence and the
364
379
  * form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and
365
380
  * 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.
381
+ * - **Visibility is honored.** A `hidden` field and a field in `form.disabled` are skipped; a
382
+ * `locked` field renders read-only; entering a new group writes its label as a section header.
369
383
  * - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring
370
384
  * fields the walk can edit and submits again. When every erroring field is one the walk cannot
371
385
  * edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could
@@ -382,7 +396,7 @@ export declare class Terminal implements TerminalInterface {
382
396
  }
383
397
 
384
398
  /**
385
- * Options for {@link import('./factories.js').createTerminal} — every member optional, so a bare
399
+ * Configures {@link import('./factories.js').createTerminal} — every member optional, so a bare
386
400
  * `createTerminal()` walks a form over the real `process.stdin` / `process.stdout` with the default
387
401
  * theme.
388
402
  *
@@ -392,27 +406,29 @@ export declare class Terminal implements TerminalInterface {
392
406
  * {@link import('./helpers.js').isInputStream}, so a test injects a fake TTY that emits scripted
393
407
  * `'data'` chunks. A stream that is not a TTY falls back to `node:readline` line input.
394
408
  * - `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.
409
+ * {@link import('@orkestrel/console/server').StreamTargetInterface}-shaped stream is accepted,
410
+ * resolved through {@link import('@orkestrel/console/server').isStreamTarget}, so a test records
411
+ * the rendered output. That is the console module's own boundary shape, so one target drives a
412
+ * console sink and this driver alike.
397
413
  * - `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
414
+ * {@link import('@orkestrel/terminal').DEFAULT_PROMPT_THEME} leaf by leaf by
415
+ * {@link import('@orkestrel/terminal').createPromptTheme}. Supplying one icon or one role leaves every
400
416
  * other slot at its default.
401
417
  */
402
418
  export declare interface TerminalOptions {
403
419
  readonly input?: InputStreamInterface;
404
- readonly output?: OutputStreamInterface;
420
+ readonly output?: StreamTargetInterface;
405
421
  readonly theme?: PromptThemeOptions;
406
422
  }
407
423
 
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. */
424
+ /**
425
+ * Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a
426
+ * reader sees why one is missing from the list it heads.
427
+ */
409
428
  export declare const UNAVAILABLE_LEAD = "Unavailable";
410
429
 
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
430
  /**
415
- * Project one held answer into the text a read-only line shows — a scalar as itself, a boolean as
431
+ * Projects one held answer into the text a read-only line shows — a scalar as itself, a boolean as
416
432
  * `yes` / `no` (the word the confirm reducer commits), and a list joined by commas. Absence renders
417
433
  * as nothing, because a locked field nobody has answered has nothing to show.
418
434
  *