@orkestrel/terminal 0.0.14 → 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,20 +1,20 @@
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 { StreamTargetInterface } from '@orkestrel/console/server';
10
- import { StylerInterface } from '@orkestrel/console';
11
- import { TerminalInterface } from '@orkestrel/terminal';
12
- import { TextField } from '@orkestrel/form';
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';
13
13
 
14
14
  /**
15
- * Erases from the cursor down to the end of the screen (`ESC[J`) — wipes the WHOLE previous (possibly
16
- * multi-line `select` / `checkbox`) view in one write before the new view is rendered, so a redraw
17
- * 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.
18
18
  */
19
19
  export declare const CLEAR_DOWN: string;
20
20
 
@@ -28,12 +28,12 @@ export declare const CLEAR_DOWN: string;
28
28
  export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string>>>;
29
29
 
30
30
  /**
31
- * Creates the interactive terminal form driver — the local-keyboard arm of the terminal trio, beside
32
- * the core headless `createPrompt` broker and the SSE `createPromptClient` bridge. Where the broker
33
- * PARKS a live form until somebody elsewhere answers it, a `Terminal` answers one HERE: it walks the
34
- * form's fields in schema order, drives each control's pure reducer over raw-mode stdin, binds every
35
- * answer through the form's own `fill`, and submits. It is the only impure part of the terminal
36
- * 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.
37
37
  *
38
38
  * @param options - See {@link TerminalOptions}
39
39
  * @returns A {@link TerminalInterface} whose `ask` drives one whole form over the resolved streams
@@ -53,29 +53,47 @@ export declare const CONTROL_HINTS: Readonly<Partial<Record<FieldControl, string
53
53
  * - **Non-TTY fallback.** A piped stream cannot enter raw mode, so the same walk runs over
54
54
  * `node:readline` line input.
55
55
  *
56
- * @example
56
+ * @example Ask one form at this keyboard
57
57
  * ```ts
58
58
  * import { createForm } from '@orkestrel/form'
59
+ * import { isTerminalError } from '@orkestrel/terminal'
59
60
  * import { createTerminal } from '@orkestrel/terminal/server'
60
61
  *
61
- * const terminal = createTerminal()
62
- * const values = await terminal.ask(
63
- * createForm({
64
- * label: 'Sign up',
65
- * fields: [
66
- * { control: 'text', name: 'email', label: 'Email', rule: { required: true, email: true } },
67
- * { control: 'confirm', name: 'terms', label: 'I accept the terms' },
68
- * ],
69
- * }),
70
- * )
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
+ * }
71
89
  * ```
72
90
  */
73
91
  export declare function createTerminal(options?: TerminalOptions): TerminalInterface;
74
92
 
75
93
  /**
76
- * Holds the cursor-UP sequence TEMPLATE (`ESC[{count}A`) — {@link import('./helpers.js').renderCursorUp}
77
- * interpolates the `{count}` placeholder with the number of lines to climb. Kept as a template so
78
- * 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.
79
97
  */
80
98
  export declare const CSI_UP: string;
81
99
 
@@ -105,11 +123,11 @@ export declare const FALLBACK_EDITOR_HINT = "(end of input finishes)";
105
123
  export declare const FALLBACK_SELECT_HINT = "Enter a number";
106
124
 
107
125
  /**
108
- * Projects any field the walk reads as a LINE OF TEXT into the {@link TextField} the text reducer
109
- * takes — `text` itself, and the controls a terminal has no widget for: `number`, `date`,
110
- * `time`, `datetime`, `color`, and one `file` entry. The label carries that control's format cue
111
- * from {@link CONTROL_HINTS}, and a declared `default` becomes the line a bare return submits. The
112
- * projection carries no rule, because the AUTHORITATIVE form still evaluates the answer this line
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
113
131
  * binds; it exists only so one reducer covers every one of them.
114
132
  *
115
133
  * @param field - The field being read
@@ -127,7 +145,7 @@ export declare function fieldToText(field: FormField): TextField;
127
145
  export declare const FILE_HINT = "One path per line, blank to finish";
128
146
 
129
147
  /**
130
- * Returns the choices a `select` or `checkbox` field SHOWS but refuses — the complement of
148
+ * Returns the choices a `select` or `checkbox` field shows but refuses — the complement of
131
149
  * {@link filterEnabled}, rendered by {@link renderUnavailableLine} above the list so a reader sees
132
150
  * why a declared choice is missing from it.
133
151
  *
@@ -137,7 +155,7 @@ export declare const FILE_HINT = "One path per line, blank to finish";
137
155
  export declare function filterDisabled(choices: readonly FieldChoice[]): readonly FieldChoice[];
138
156
 
139
157
  /**
140
- * Returns the choices a `select` or `checkbox` field actually OFFERS — the form refuses a disabled
158
+ * Returns the choices a `select` or `checkbox` field actually offers — the form refuses a disabled
141
159
  * choice's value at every door, including a fill, so the walk never puts one in front of the
142
160
  * cursor. Pair with {@link filterDisabled} to tell the reader what was withheld.
143
161
  *
@@ -166,24 +184,30 @@ export declare function filterEnabled(choices: readonly FieldChoice[]): readonly
166
184
  * - `isTTY` — `true` on a real terminal, absent or `false` when piped to a file or another process.
167
185
  */
168
186
  export declare interface InputStreamInterface {
187
+ /** Subscribes a `'data'` chunk listener — the irreducible event seam. */
169
188
  on(event: 'data', listener: (chunk: string | Uint8Array) => void): void;
189
+ /** Unsubscribes that listener. The driver always pairs it, so no listener leaks. */
170
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. */
171
192
  setRawMode?(mode: boolean): void;
193
+ /** Starts the flow of `'data'` events. */
172
194
  resume?(): void;
195
+ /** Stops it again on cleanup. */
173
196
  pause?(): void;
197
+ /** Reports whether the stream is a real terminal; absent or false when piped. */
174
198
  readonly isTTY?: boolean;
175
199
  }
176
200
 
177
201
  /**
178
- * Checks whether `value` is a usable {@link InputStreamInterface} — a record with callable `on` / `off`
179
- * `'data'` subscription methods. A total type guard: it NEVER throws and returns `false`
180
- * for anything off-shape, so it narrows the one unavoidable input boundary (the real `process.stdin`,
181
- * 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.
182
206
  *
183
207
  * @remarks
184
208
  * Only `on` / `off` are required (the irreducible event seam); `setRawMode` / `resume` / `pause` /
185
209
  * `isTTY` are optional on {@link InputStreamInterface}, so their absence does not disqualify a stream
186
- * — 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
187
211
  * fallback rather than raw mode.
188
212
  *
189
213
  * @param value - Any value crossing the boundary (a process stream, an injected fake, `unknown`)
@@ -193,11 +217,11 @@ export declare function isInputStream(value: unknown): value is InputStreamInter
193
217
 
194
218
  /**
195
219
  * Checks whether `value` is a Node {@link NodeJS.ReadableStream} — a total structural guard
196
- * checking for the callable `read` / `pipe` / `on` that `node:readline`'s `createInterface` requires
197
- * as its `input`. The non-TTY fallback narrows the resolved input stream through this before handing
198
- * it to readline (never an `as`), so a real piped `process.stdin` (or a `PassThrough` a test injects)
199
- * crosses into the readline boundary honestly. Never throws; returns `false` for a minimal fake that
200
- * isn't a full readable.
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.
201
225
  *
202
226
  * @param value - The resolved input stream (or any value crossing the boundary)
203
227
  * @returns True if `value` has the readable methods readline needs; false otherwise
@@ -205,10 +229,10 @@ export declare function isInputStream(value: unknown): value is InputStreamInter
205
229
  export declare function isReadable(value: unknown): value is NodeJS.ReadableStream;
206
230
 
207
231
  /**
208
- * Counts the terminal LINES a rendered prompt `view` occupies — one more than its newline count
209
- * (a view with no newline is a single line; N newlines span N+1 lines). The basis of the in-place
210
- * re-render: the driver records the line count of the view it just wrote so the next redraw knows how
211
- * 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.
212
236
  *
213
237
  * @param view - The rendered (possibly multi-line, possibly ANSI-styled) view string
214
238
  * @returns The number of lines the view spans (always at least 1)
@@ -219,20 +243,20 @@ export declare function lineCount(view: string): number;
219
243
  export declare const LOCKED_MARK = "(locked)";
220
244
 
221
245
  /**
222
- * Returns the full reposition-and-clear prefix to write BEFORE re-rendering a prompt view in place — given
223
- * the line count of the PREVIOUS view, it moves the cursor up over those lines, returns it to column
224
- * 0, and erases everything from there to the end of the screen, so the next view is drawn on a clean
225
- * region (no orphaned rows from a taller previous view). Pure; the driver writes this immediately
226
- * followed by the new view.
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.
227
251
  *
228
252
  * @remarks
229
- * For the FIRST render `previousLines` is `1` (the cursor sits on the line the prompt opened on) so
230
- * the prefix is just a carriage return + clear-down — the prompt draws from the current line. For a
231
- * subsequent render it climbs `previousLines - 1` lines (the cursor is on the LAST line of the prior
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
232
256
  * view) before clearing. Keeping the math here (not in the driver) makes the re-render unit-testable
233
257
  * without a real terminal.
234
258
  *
235
- * @param previousLines - The line count of the view currently on screen (from {@link lineCount})
259
+ * @param previousLines - The line count of the view on screen (from {@link lineCount})
236
260
  * @returns The control-sequence prefix to write before the new view
237
261
  */
238
262
  export declare function redrawPrefix(previousLines: number): string;
@@ -246,10 +270,10 @@ export declare function redrawPrefix(previousLines: number): string;
246
270
  export declare const REFUSAL_MESSAGE = "Enter a value this field accepts";
247
271
 
248
272
  /**
249
- * Returns the cursor-UP control sequence that moves the cursor up `count` lines (`ESC[{count}A`) — or the
250
- * empty string when `count` is zero or negative (no movement needed, and `ESC[0A` is a wasted write).
251
- * The pure step the in-place re-render uses to climb back over the previous view before clearing it.
252
- * 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.
253
277
  *
254
278
  * @param count - How many lines to move the cursor up
255
279
  * @returns The `ESC[{count}A` sequence, or `''` when `count <= 0`
@@ -268,9 +292,9 @@ export declare function renderCursorUp(count: number): string;
268
292
  export declare function renderGroupHeader(styler: StylerInterface, theme: PromptTheme, label: string): string;
269
293
 
270
294
  /**
271
- * Renders the read-only line a LOCKED field shows — its label, the {@link LOCKED_MARK}, and the
272
- * answer the form already holds. The walk writes this instead of a prompt, because the field is still
273
- * validated and still submitted but must not be edited here.
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.
274
298
  *
275
299
  * @param styler - The console styler that renders each role
276
300
  * @param theme - The resolved prompt theme
@@ -293,7 +317,7 @@ export declare function renderLockedLine(styler: StylerInterface, theme: PromptT
293
317
  export declare function renderNumberedList(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
294
318
 
295
319
  /**
296
- * Renders the line listing an OPEN select's offered values above its text prompt — a suggestion
320
+ * Renders the line listing an open select's offered values above its text prompt — a suggestion
297
321
  * list, because an open select admits an answer the list does not offer.
298
322
  *
299
323
  * @param styler - The console styler that renders each role
@@ -314,15 +338,18 @@ export declare function renderSuggestionLine(styler: StylerInterface, theme: Pro
314
338
  */
315
339
  export declare function renderUnavailableLine(styler: StylerInterface, theme: PromptTheme, choices: readonly FieldChoice[]): string;
316
340
 
317
- /** Holds 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
+ */
318
345
  export declare const SUGGESTION_LEAD = "Suggestions";
319
346
 
320
347
  /**
321
- * Checks whether an input stream can be driven in RAW mode — it both reports `isTTY === true` AND
322
- * exposes a callable `setRawMode`. The {@link import('./Terminal.js').Terminal} probes this to choose its path:
323
- * `true` ⇒ the interactive raw-mode prompts (arrow-key navigation, live re-render); `false` ⇒ the
324
- * `node:readline` line-input fallback (a piped / non-terminal stream cannot enter raw mode). Total —
325
- * never throws.
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.
326
353
  *
327
354
  * @param input - The resolved {@link InputStreamInterface}
328
355
  * @returns True if the stream is a TTY with `setRawMode`; false otherwise
@@ -330,12 +357,13 @@ export declare const SUGGESTION_LEAD = "Suggestions";
330
357
  export declare function supportsRawMode(input: InputStreamInterface): boolean;
331
358
 
332
359
  /**
333
- * Implements {@link TerminalInterface} for a human at this machine's keyboard — the interactive form
334
- * DRIVER, and the only impure part of the terminal stack. {@link ask} walks one
335
- * form's fields in schema order, feeds raw-mode stdin bytes through `parseKey` into the matching
336
- * pure reducer, renders each returned view in place, and binds every answer through the form's own
337
- * `fill`. It owns no form logic: the schema, the rules, the values, and the settlement all belong to
338
- * 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.
339
367
  *
340
368
  * @remarks
341
369
  * See {@link TerminalInterface} for the driving contract. The walk itself:
@@ -347,12 +375,11 @@ export declare function supportsRawMode(input: InputStreamInterface): boolean;
347
375
  * `open` means the answer need not come from the list.
348
376
  * - **The binding projects through `matchesAnswer`.** Every answer is filled as
349
377
  * `fill(name, matchesAnswer(value) ? value : undefined)` after `parseValue` has coerced it to the
350
- * 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
351
379
  * form's `required` rule refuses it. A typed answer the control cannot hold binds as absence and
352
380
  * invalidates the field, so the walk asks again with the reason on screen.
353
- * - **Visibility is honored.** A `hidden` field and a field currently in `form.disabled` are
354
- * skipped; a `locked` field renders read-only; entering a new group writes its label as a section
355
- * 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.
356
383
  * - **Refusal re-asks.** After the walk the form is submitted. A refusal re-walks only the erroring
357
384
  * fields the walk can edit and submits again. When every erroring field is one the walk cannot
358
385
  * edit — hidden, locked, or disabled — the form is abandoned instead, because asking again could
@@ -384,8 +411,8 @@ export declare class Terminal implements TerminalInterface {
384
411
  * the rendered output. That is the console module's own boundary shape, so one target drives a
385
412
  * console sink and this driver alike.
386
413
  * - `theme` — the glyphs and role styles every rendered line is painted with, merged over
387
- * {@link import('@src/core').DEFAULT_PROMPT_THEME} leaf by leaf by
388
- * {@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
389
416
  * other slot at its default.
390
417
  */
391
418
  export declare interface TerminalOptions {
@@ -394,7 +421,10 @@ export declare interface TerminalOptions {
394
421
  readonly theme?: PromptThemeOptions;
395
422
  }
396
423
 
397
- /** Holds the lead on the line listing the choices a `select` or `checkbox` shows but refuses, so a reader sees why one is missing from the list below. */
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
+ */
398
428
  export declare const UNAVAILABLE_LEAD = "Unavailable";
399
429
 
400
430
  /**