@orkestrel/terminal 0.0.7 → 0.0.8

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,5 +1,5 @@
1
- import { isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, rawShape, recordOf, stringShape } from "@orkestrel/contract";
2
- import { STATUS_ICONS, createStyler, stripControls } from "@orkestrel/console";
1
+ import { ATTRIBUTES, COLORS, STATUS_ICONS, createStyler, freezeStyle, stripControls } from "@orkestrel/console";
2
+ import { FORMAT_PATTERNS, arrayOf, isArray, isBoolean, isNonEmptyString, isNumber, isRecord, isString, literalOf, parseJSON, rawShape, recordOf, stringShape } from "@orkestrel/contract";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  import { createSSEParser } from "@orkestrel/sse";
5
5
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
@@ -129,9 +129,10 @@ var CONTROL_NAMES = Object.freeze({
129
129
  });
130
130
  /** The default mask glyph a {@link import('./types.js').PasswordState} renders each input character as — `*`. */
131
131
  var DEFAULT_MASK = "*";
132
- /** Matches an email address — a non-trivial `local@domain.tld` shape. The `email` rule tests against this. */
133
- var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
134
- /** Matches an HTTP(S) URL. The `url` rule tests against this. */
132
+ /**
133
+ * Matches an HTTP(S) URL. The `url` rule tests against this. Kept locally because it is
134
+ * narrower than `FORMAT_PATTERNS.uri`, which accepts other URI schemes.
135
+ */
135
136
  var URL_PATTERN = /^https?:\/\/.+/;
136
137
  /** Matches a numeric value (integer or decimal, optional sign). The `numeric` rule tests against this. */
137
138
  var NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
@@ -158,11 +159,8 @@ var RULE_MESSAGES = Object.freeze({
158
159
  invalid: "Invalid input"
159
160
  });
160
161
  /**
161
- * The prompt-view icon glyphs the reducers render the prompt line and choice rows with. PLAIN
162
- * glyphs — color is applied by the {@link import('./types.js').PromptState}'s
163
- * {@link import('@orkestrel/console').StylerInterface} at render time, never baked into the
164
- * constant (AGENTS — styling orthogonal to data; the rework fixes the terminal's ANSI-in-the-icon).
165
- * Frozen.
162
+ * The six default glyphs {@link DEFAULT_PROMPT_THEME} assembles its `icons` from. Read only when
163
+ * the default theme is assembled; a view reads its resolved theme and never this constant. Frozen.
166
164
  *
167
165
  * @remarks
168
166
  * - `question` — the leading mark on a prompt's message line.
@@ -178,6 +176,79 @@ var PROMPT_ICONS = Object.freeze({
178
176
  checked: "☑",
179
177
  unchecked: "☐"
180
178
  });
179
+ /**
180
+ * Every {@link import('./types.js').PromptRole}, in one frozen list — the role axis's source of
181
+ * truth. {@link import('./helpers.js').createPromptTheme} walks it to merge a partial theme, and
182
+ * a consumer building a complete role map reads it rather than retyping every name.
183
+ */
184
+ var PROMPT_ROLES = Object.freeze([
185
+ "question",
186
+ "pointer",
187
+ "message",
188
+ "content",
189
+ "success",
190
+ "error",
191
+ "selected",
192
+ "focus",
193
+ "hint",
194
+ "muted",
195
+ "description"
196
+ ]);
197
+ /**
198
+ * The {@link import('./types.js').PromptTheme} every prompt renders with unless its options supply
199
+ * another — the glyph set assembled from {@link PROMPT_ICONS} plus the console
200
+ * {@link import('@orkestrel/console').STATUS_ICONS} `success` / `error` marks, and the console
201
+ * {@link import('@orkestrel/console').Style} each role is painted with. Deeply frozen through the
202
+ * console module's own {@link import('@orkestrel/console').freezeStyle}; the baseline
203
+ * {@link import('./helpers.js').createPromptTheme} merges a partial theme over.
204
+ *
205
+ * @remarks
206
+ * The default roles reproduce the views' historical coloring exactly: `question` / `pointer` cyan,
207
+ * `message` / `focus` bold, `success` / `selected` green, `error` red, `hint` / `muted` /
208
+ * `description` dim, and `content` the EMPTY style — an empty style renders bare text, so primary
209
+ * content keeps the bytes it had before it became themeable.
210
+ * Two roles sharing a style today are still two roles — re-mapping one leaves the other alone.
211
+ */
212
+ var DEFAULT_PROMPT_THEME = Object.freeze({
213
+ icons: Object.freeze({
214
+ question: PROMPT_ICONS.question,
215
+ pointer: PROMPT_ICONS.pointer,
216
+ dot: PROMPT_ICONS.dot,
217
+ selected: PROMPT_ICONS.selected,
218
+ checked: PROMPT_ICONS.checked,
219
+ unchecked: PROMPT_ICONS.unchecked,
220
+ success: STATUS_ICONS.success,
221
+ error: STATUS_ICONS.error
222
+ }),
223
+ roles: Object.freeze({
224
+ question: freezeStyle({
225
+ foreground: "cyan",
226
+ attributes: []
227
+ }),
228
+ pointer: freezeStyle({
229
+ foreground: "cyan",
230
+ attributes: []
231
+ }),
232
+ message: freezeStyle({ attributes: ["bold"] }),
233
+ content: freezeStyle({ attributes: [] }),
234
+ success: freezeStyle({
235
+ foreground: "green",
236
+ attributes: []
237
+ }),
238
+ error: freezeStyle({
239
+ foreground: "red",
240
+ attributes: []
241
+ }),
242
+ selected: freezeStyle({
243
+ foreground: "green",
244
+ attributes: []
245
+ }),
246
+ focus: freezeStyle({ attributes: ["bold"] }),
247
+ hint: freezeStyle({ attributes: ["dim"] }),
248
+ muted: freezeStyle({ attributes: ["dim"] }),
249
+ description: freezeStyle({ attributes: ["dim"] })
250
+ })
251
+ });
181
252
  /** How long (ms) the {@link import('./types.js').PromptInterface} broker parks an unanswered prompt before it expires — 5 minutes. */
182
253
  var DEFAULT_PROMPT_TIMEOUT_MS = 3e5;
183
254
  /** How long (ms) the {@link import('./types.js').PromptClientInterface} waits before each reconnect attempt — 2 seconds. */
@@ -271,6 +342,63 @@ var isPendingPrompt = recordOf({
271
342
  from: isString,
272
343
  to: isString
273
344
  }, ["from", "to"]);
345
+ /**
346
+ * Narrow an unknown value to a console {@link Style} — the value every
347
+ * {@link import('./types.js').PromptRole} carries, built from the console module's own
348
+ * {@link import('@orkestrel/console').COLORS} + {@link import('@orkestrel/console').ATTRIBUTES} so
349
+ * the style vocabulary has ONE source.
350
+ *
351
+ * @remarks
352
+ * The record is CLOSED and matches the `Style` contract exactly: `foreground` and `background` are
353
+ * optional and each must name a color (`default` included, as the console `Color` union has it),
354
+ * `attributes` is REQUIRED and must be an array over the attribute set, and any other key rejects
355
+ * the whole value.
356
+ */
357
+ var isStyle = recordOf({
358
+ foreground: literalOf(...COLORS, "default"),
359
+ background: literalOf(...COLORS, "default"),
360
+ attributes: arrayOf(literalOf(...ATTRIBUTES))
361
+ }, ["foreground", "background"]);
362
+ /**
363
+ * Narrow an unknown wire value to a {@link PromptThemeOptions} — the §14 guard a remote prompt's
364
+ * `theme` option passes before it reaches a local prompt.
365
+ *
366
+ * @remarks
367
+ * The shape is CLOSED at every level: an unknown icon slot, an unknown role, a non-string glyph, or
368
+ * a role whose value is not a {@link isStyle}-shaped console style rejects the whole theme, so a
369
+ * hostile payload degrades to the default theme rather than partially applying. Glyphs still carry
370
+ * arbitrary text at this point — {@link import('./helpers.js').sanitizeThemeIcons} strips their
371
+ * control bytes on dispatch.
372
+ *
373
+ * The icons and roles shapes are checked against their complete union-keyed guard records. A slot
374
+ * added to either union without an entry here is a compile error rather than a wire theme that is
375
+ * silently refused.
376
+ */
377
+ var isPromptThemeOptions = recordOf({
378
+ icons: recordOf({
379
+ question: isString,
380
+ pointer: isString,
381
+ dot: isString,
382
+ selected: isString,
383
+ checked: isString,
384
+ unchecked: isString,
385
+ success: isString,
386
+ error: isString
387
+ }, true),
388
+ roles: recordOf({
389
+ question: isStyle,
390
+ pointer: isStyle,
391
+ message: isStyle,
392
+ content: isStyle,
393
+ success: isStyle,
394
+ error: isStyle,
395
+ selected: isStyle,
396
+ focus: isStyle,
397
+ hint: isStyle,
398
+ description: isStyle,
399
+ muted: isStyle
400
+ }, true)
401
+ }, true);
274
402
  /** Narrow an unknown value to a {@link TerminalSnapshot}. */
275
403
  var isTerminalSnapshot = recordOf({
276
404
  id: isNonEmptyString,
@@ -395,7 +523,7 @@ function evaluateRule(rule, check, input) {
395
523
  }
396
524
  break;
397
525
  case "email":
398
- if (check === true && !EMAIL_PATTERN.test(input)) return RULE_MESSAGES.email;
526
+ if (check === true && !FORMAT_PATTERNS.email.test(input)) return RULE_MESSAGES.email;
399
527
  break;
400
528
  case "url":
401
529
  if (check === true && !URL_PATTERN.test(input)) return RULE_MESSAGES.url;
@@ -500,33 +628,116 @@ function normalizeCheckboxChoice(choice) {
500
628
  value: choice
501
629
  } : choice;
502
630
  }
503
- /** The styled prompt-message header (`? message`) — the leading line every active prompt view shares. */
504
- function promptHeader(styler, message) {
505
- return `${styler.cyan(PROMPT_ICONS.question)} ${styler.bold(message)}`;
631
+ /**
632
+ * Build a complete {@link PromptTheme} by merging a partial one over
633
+ * {@link DEFAULT_PROMPT_THEME}, leaf by leaf — each supplied icon replaces that glyph, each
634
+ * supplied role replaces that {@link Style}, and everything else keeps its default. Each supplied
635
+ * style is snapshotted through the console module's own
636
+ * {@link import('@orkestrel/console').freezeStyle}, so the result is deeply frozen and a caller
637
+ * mutating its own attribute list afterwards cannot reach into a built theme.
638
+ *
639
+ * @param options - The partial theme to merge, or `undefined` for the defaults
640
+ * @returns The resolved, deeply frozen theme every prompt state carries
641
+ *
642
+ * @example
643
+ * ```ts
644
+ * createPromptTheme() // the defaults
645
+ * createPromptTheme({
646
+ * icons: { pointer: '=>' },
647
+ * roles: { message: { foreground: 'magenta', attributes: ['bold'] } },
648
+ * })
649
+ * ```
650
+ */
651
+ function createPromptTheme(options) {
652
+ const icons = {
653
+ ...DEFAULT_PROMPT_THEME.icons,
654
+ ...options?.icons
655
+ };
656
+ const roles = { ...DEFAULT_PROMPT_THEME.roles };
657
+ for (const role of PROMPT_ROLES) {
658
+ const style = options?.roles?.[role];
659
+ if (style !== void 0) roles[role] = freezeStyle(style);
660
+ }
661
+ return Object.freeze({
662
+ icons: Object.freeze(icons),
663
+ roles: Object.freeze(roles)
664
+ });
506
665
  }
507
- /** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved. */
508
- function submitHeader(styler, message) {
509
- return `${styler.green(STATUS_ICONS.success)} ${styler.bold(message)}`;
666
+ /**
667
+ * Sanitize text for one single-line display slot. Composes console's {@link stripControls} pass
668
+ * with removal of tab, line feed, and carriage return, so no C0 control or DEL survives.
669
+ *
670
+ * @param text - The text to sanitize for a glyph or hint slot
671
+ * @returns The text with every C0 control character and DEL removed
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * sanitizeDisplayText('Q\rOVERWRITE\nNEXT\tX') // 'QOVERWRITENEXTX'
676
+ * ```
677
+ */
678
+ function sanitizeDisplayText(text) {
679
+ return stripControls(text).replaceAll(" ", "").replaceAll("\n", "").replaceAll("\r", "");
680
+ }
681
+ /**
682
+ * Sanitize every glyph a wire-supplied {@link PromptThemeOptions} carries for a single-line display
683
+ * slot. Only the icons need it: a role is guard-narrowed to a console {@link Style}, whose colors
684
+ * and attributes are fixed name sets, so no role can carry a byte a terminal would act on.
685
+ *
686
+ * @param theme - The narrowed theme options a remote prompt supplied
687
+ * @returns The same theme with every supplied glyph sanitized for a single-line display slot
688
+ */
689
+ function sanitizeThemeIcons(theme) {
690
+ const icons = theme.icons;
691
+ if (icons === void 0) return theme;
692
+ const sanitized = {};
693
+ for (const [icon, glyph] of Object.entries(icons)) sanitized[icon] = sanitizeDisplayText(glyph);
694
+ return {
695
+ ...theme,
696
+ icons: sanitized
697
+ };
698
+ }
699
+ /** The styled prompt-message header (`? message`) — the leading line every active prompt view shares, themed by the `question` + `message` roles. */
700
+ function promptHeader(styler, theme, message) {
701
+ return `${styler.render(theme.roles.question, theme.icons.question)} ${styler.render(theme.roles.message, message)}`;
702
+ }
703
+ /**
704
+ * Render a prompt header followed by a key hint painted with the `hint` role, or the header alone
705
+ * when no hint is supplied.
706
+ *
707
+ * @param styler - The console styler that renders each role
708
+ * @param theme - The resolved prompt theme
709
+ * @param message - The prompt's question text
710
+ * @param hint - The optional key hint to append
711
+ * @returns The rendered header with the optional hint
712
+ */
713
+ function hintedHeader(styler, theme, message, hint) {
714
+ const head = promptHeader(styler, theme, message);
715
+ return hint === void 0 ? head : `${head} ${styler.render(theme.roles.hint, hint)}`;
716
+ }
717
+ /** The styled submit line (`✔ message`) — the committed header an interactive prompt shows once resolved, themed by the `success` + `message` roles. */
718
+ function submitHeader(styler, theme, message) {
719
+ return `${styler.render(theme.roles.success, theme.icons.success)} ${styler.render(theme.roles.message, message)}`;
510
720
  }
511
- /** The styled error line (`✖ message`) — appended beneath a prompt view when the last submit failed validation. */
512
- function errorLine(styler, message) {
513
- return `${styler.red(STATUS_ICONS.error)} ${styler.red(message)}`;
721
+ /** The styled error line (`✖ message`) — appended beneath a prompt view when the last submit failed validation, themed by the `error` role. */
722
+ function errorLine(styler, theme, message) {
723
+ return `${styler.render(theme.roles.error, theme.icons.error)} ${styler.render(theme.roles.error, message)}`;
514
724
  }
515
- /** Build the initial {@link InputState} from {@link InputOptions} — resolving the validator + styler, seeding an empty value. */
725
+ /** Build the initial {@link InputState} from {@link InputOptions} — resolving the validator + styler + theme, seeding an empty value. */
516
726
  function createInputState(options) {
517
727
  return {
518
728
  message: options.message,
519
729
  default: options.default ?? "",
520
730
  validator: resolveValidation(options.validate),
521
731
  styler: options.styler ?? createStyler(),
732
+ theme: createPromptTheme(options.theme),
522
733
  value: ""
523
734
  };
524
735
  }
525
- /** Render an {@link InputState} as a styled view — the header, the typed value (or dimmed default hint), and any error. */
736
+ /** Render an {@link InputState} as a styled view — the header, the typed value painted by the `content` role (or the default painted as a hint), and any error. */
526
737
  function inputView(state) {
527
- const shown = state.value.length > 0 ? state.value : state.styler.dim(state.default);
528
- const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${shown}`;
529
- return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
738
+ const shown = state.value.length > 0 ? state.styler.render(state.theme.roles.content, state.value) : state.styler.render(state.theme.roles.hint, state.default);
739
+ const head = `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${shown}`;
740
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.theme, state.error)}`;
530
741
  }
531
742
  /**
532
743
  * Advance an input prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -560,7 +771,7 @@ function inputReduce(state, key) {
560
771
  ...clear,
561
772
  value: answer
562
773
  },
563
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer)}`,
774
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer)}`,
564
775
  status: "submit",
565
776
  value: answer
566
777
  };
@@ -581,21 +792,22 @@ function inputReduce(state, key) {
581
792
  status: "active"
582
793
  };
583
794
  }
584
- /** Build the initial {@link PasswordState} from {@link PasswordOptions} — resolving the validator + styler + mask. */
795
+ /** Build the initial {@link PasswordState} from {@link PasswordOptions} — resolving the validator + styler + theme + mask. */
585
796
  function createPasswordState(options) {
586
797
  return {
587
798
  message: options.message,
588
799
  mask: options.mask ?? "*",
589
800
  validator: resolveValidation(options.validate),
590
801
  styler: options.styler ?? createStyler(),
802
+ theme: createPromptTheme(options.theme),
591
803
  value: ""
592
804
  };
593
805
  }
594
- /** Render a {@link PasswordState} as a styled view — the header, the value masked to `mask` repeated, and any error. */
806
+ /** Render a {@link PasswordState} as a styled view — the header, the value masked to `mask` repeated and painted by the `content` role, and any error. */
595
807
  function passwordView(state) {
596
- const masked = state.mask.repeat(state.value.length);
597
- const head = `${promptHeader(state.styler, state.message)} ${state.styler.cyan(PROMPT_ICONS.pointer)} ${masked}`;
598
- return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.error)}`;
808
+ const masked = state.styler.render(state.theme.roles.content, state.mask.repeat(state.value.length));
809
+ const head = `${promptHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer)} ${masked}`;
810
+ return state.error === void 0 ? head : `${head}\n${errorLine(state.styler, state.theme, state.error)}`;
599
811
  }
600
812
  /**
601
813
  * Advance a password prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -625,7 +837,7 @@ function passwordReduce(state, key) {
625
837
  }
626
838
  return {
627
839
  state: clear,
628
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(state.mask.repeat(state.value.length))}`,
840
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, state.mask.repeat(state.value.length))}`,
629
841
  status: "submit",
630
842
  value: state.value
631
843
  };
@@ -646,18 +858,26 @@ function passwordReduce(state, key) {
646
858
  status: "active"
647
859
  };
648
860
  }
649
- /** Build the initial {@link ConfirmState} from {@link ConfirmOptions} — defaulting the answer to `false`. */
861
+ /** Build the initial {@link ConfirmState} from {@link ConfirmOptions} — defaulting the answer to `false`, resolving the styler + theme, carrying any hint override. */
650
862
  function createConfirmState(options) {
651
863
  return {
652
864
  message: options.message,
653
865
  default: options.default ?? false,
654
- styler: options.styler ?? createStyler()
866
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
867
+ styler: options.styler ?? createStyler(),
868
+ theme: createPromptTheme(options.theme)
655
869
  };
656
870
  }
657
- /** Render a {@link ConfirmState} as a styled view — the header plus a `(Y/n)` hint with the default letter emphasized. */
871
+ /**
872
+ * Render a {@link ConfirmState} as a styled view — the header plus the `(Y/n)` group with the
873
+ * default letter painted by the `selected` role. A supplied `hint` replaces that whole group,
874
+ * parentheses included, painted by the `hint` role.
875
+ */
658
876
  function confirmView(state) {
659
- const hint = state.default ? `${state.styler.green("Y")}${state.styler.dim("/n")}` : `${state.styler.dim("y/")}${state.styler.green("N")}`;
660
- return `${promptHeader(state.styler, state.message)} ${state.styler.dim("(")}${hint}${state.styler.dim(")")}`;
877
+ if (state.hint !== void 0) return hintedHeader(state.styler, state.theme, state.message, state.hint);
878
+ const head = promptHeader(state.styler, state.theme, state.message);
879
+ const answer = state.default ? `${state.styler.render(state.theme.roles.selected, "Y")}${state.styler.render(state.theme.roles.hint, "/n")}` : `${state.styler.render(state.theme.roles.hint, "y/")}${state.styler.render(state.theme.roles.selected, "N")}`;
880
+ return `${head} ${state.styler.render(state.theme.roles.hint, "(")}${answer}${state.styler.render(state.theme.roles.hint, ")")}`;
661
881
  }
662
882
  /**
663
883
  * Advance a confirm prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<boolean>`
@@ -682,29 +902,31 @@ function confirmReduce(state, key) {
682
902
  };
683
903
  return {
684
904
  state,
685
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(answer ? "yes" : "no")}`,
905
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, answer ? "yes" : "no")}`,
686
906
  status: "submit",
687
907
  value: answer
688
908
  };
689
909
  }
690
- /** Build the initial {@link SelectState} from {@link SelectOptions} — normalizing choices and pre-focusing the default. */
910
+ /** Build the initial {@link SelectState} from {@link SelectOptions} — normalizing choices, resolving the styler + theme, carrying any hint, and pre-focusing the default. */
691
911
  function createSelectState(options) {
692
912
  const choices = options.choices.map(normalizeChoice);
693
913
  const index = choices.findIndex((choice) => choice.value === options.default);
694
914
  return {
695
915
  message: options.message,
696
916
  choices,
917
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
697
918
  styler: options.styler ?? createStyler(),
919
+ theme: createPromptTheme(options.theme),
698
920
  focused: index >= 0 ? index : 0
699
921
  };
700
922
  }
701
- /** Render a {@link SelectState} as a MULTI-LINE styled view — the header followed by one row per choice, the focused row marked. */
923
+ /** Render a {@link SelectState} as a MULTI-LINE styled view — the header (plus any hint) followed by one row per choice, the focused row marked. */
702
924
  function selectView(state) {
703
925
  const lines = state.choices.map((choice, index) => {
704
926
  const active = index === state.focused;
705
- return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${active ? state.styler.green(PROMPT_ICONS.selected) : state.styler.dim(PROMPT_ICONS.dot)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
927
+ return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${active ? state.styler.render(state.theme.roles.selected, state.theme.icons.selected) : state.styler.render(state.theme.roles.muted, state.theme.icons.dot)} ${active ? state.styler.render(state.theme.roles.focus, choice.name) : state.styler.render(state.theme.roles.content, choice.name)}${choice.description === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.description)}`}`;
706
928
  });
707
- return [promptHeader(state.styler, state.message), ...lines].join("\n");
929
+ return [hintedHeader(state.styler, state.theme, state.message, state.hint), ...lines].join("\n");
708
930
  }
709
931
  /**
710
932
  * Advance a select prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -751,7 +973,7 @@ function selectReduce(state, key) {
751
973
  const value = choice?.value ?? "";
752
974
  return {
753
975
  state,
754
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(choice?.name ?? "")}`,
976
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, choice?.name ?? "")}`,
755
977
  status: "submit",
756
978
  value
757
979
  };
@@ -762,7 +984,7 @@ function selectReduce(state, key) {
762
984
  status: "active"
763
985
  };
764
986
  }
765
- /** Build the initial {@link CheckboxState} from {@link CheckboxOptions} — normalizing choices, seeding the checked set, carrying min/max. */
987
+ /** Build the initial {@link CheckboxState} from {@link CheckboxOptions} — normalizing choices, resolving the styler + theme, carrying any hint, seeding the checked set, carrying min/max. */
766
988
  function createCheckboxState(options) {
767
989
  const choices = options.choices.map(normalizeCheckboxChoice);
768
990
  const checked = choices.reduce((indices, choice, index) => {
@@ -772,27 +994,29 @@ function createCheckboxState(options) {
772
994
  return {
773
995
  message: options.message,
774
996
  choices,
997
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
775
998
  styler: options.styler ?? createStyler(),
999
+ theme: createPromptTheme(options.theme),
776
1000
  focused: 0,
777
1001
  checked,
778
1002
  ...options.min !== void 0 ? { min: options.min } : {},
779
1003
  ...options.max !== void 0 ? { max: options.max } : {}
780
1004
  };
781
1005
  }
782
- /** Render a {@link CheckboxState} as a MULTI-LINE styled view — the header, one box per choice (focused + checked marked), a count, and any error. */
1006
+ /** Render a {@link CheckboxState} as a MULTI-LINE styled view — the header (plus any hint), one box per choice (focused + checked marked), a count, and any error. */
783
1007
  function checkboxView(state) {
784
1008
  const lines = state.choices.map((choice, index) => {
785
1009
  const active = index === state.focused;
786
1010
  const ticked = state.checked.includes(index);
787
- return `${active ? state.styler.cyan(PROMPT_ICONS.pointer) : " "} ${ticked ? state.styler.green(PROMPT_ICONS.checked) : state.styler.dim(PROMPT_ICONS.unchecked)} ${active ? state.styler.bold(choice.name) : choice.name}${choice.description === void 0 ? "" : ` ${state.styler.dim(choice.description)}`}`;
1011
+ return `${active ? state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer) : " "} ${ticked ? state.styler.render(state.theme.roles.selected, state.theme.icons.checked) : state.styler.render(state.theme.roles.muted, state.theme.icons.unchecked)} ${active ? state.styler.render(state.theme.roles.focus, choice.name) : state.styler.render(state.theme.roles.content, choice.name)}${choice.description === void 0 ? "" : ` ${state.styler.render(state.theme.roles.description, choice.description)}`}`;
788
1012
  });
789
- const summary = state.styler.dim(`${state.checked.length} selected`);
1013
+ const summary = state.styler.render(state.theme.roles.hint, `${state.checked.length} selected`);
790
1014
  const body = [
791
- promptHeader(state.styler, state.message),
1015
+ hintedHeader(state.styler, state.theme, state.message, state.hint),
792
1016
  ...lines,
793
1017
  summary
794
1018
  ].join("\n");
795
- return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.error)}`;
1019
+ return state.error === void 0 ? body : `${body}\n${errorLine(state.styler, state.theme, state.error)}`;
796
1020
  }
797
1021
  /**
798
1022
  * Advance a checkbox prompt by one {@link KeyEvent} — the pure
@@ -861,7 +1085,7 @@ function checkboxReduce(state, key) {
861
1085
  const summary = ordered.map((index) => state.choices[index]?.name).filter((name) => name !== void 0).join(", ");
862
1086
  return {
863
1087
  state: clear,
864
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(summary)}`,
1088
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, summary)}`,
865
1089
  status: "submit",
866
1090
  value: values
867
1091
  };
@@ -881,21 +1105,29 @@ function gateSelection(count, min, max) {
881
1105
  if (min !== void 0 && count < min) return `Select at least ${String(min)} option${min === 1 ? "" : "s"}`;
882
1106
  if (max !== void 0 && count > max) return `Select no more than ${String(max)} option${max === 1 ? "" : "s"}`;
883
1107
  }
884
- /** Build the initial {@link EditorState} from {@link EditorOptions} — resolving the validator + styler, seeding empty lines. */
1108
+ /** Build the initial {@link EditorState} from {@link EditorOptions} — resolving the validator + styler + theme, carrying any hint override, seeding empty lines. */
885
1109
  function createEditorState(options) {
886
1110
  return {
887
1111
  message: options.message,
888
1112
  default: options.default ?? "",
1113
+ ...options.hint !== void 0 ? { hint: options.hint } : {},
889
1114
  validator: resolveValidation(options.validate),
890
1115
  styler: options.styler ?? createStyler(),
1116
+ theme: createPromptTheme(options.theme),
891
1117
  lines: [],
892
1118
  current: ""
893
1119
  };
894
1120
  }
895
- /** Render an {@link EditorState} as a MULTI-LINE styled view — the header (with a Ctrl+D hint), the committed lines, the in-progress line, and any error. */
1121
+ /**
1122
+ * Render an {@link EditorState} as a MULTI-LINE styled view — the header with its finish hint, the
1123
+ * committed and in-progress lines painted by the `content` role, and any error. A supplied `hint`
1124
+ * replaces `(Ctrl+D to finish)` in full; the finish key stays ctrl-d.
1125
+ */
896
1126
  function editorView(state) {
897
- const view = [`${promptHeader(state.styler, state.message)} ${state.styler.dim("(Ctrl+D to finish)")}`, ...[...state.lines, `${state.styler.cyan(PROMPT_ICONS.pointer)} ${state.current}`]].join("\n");
898
- return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.error)}`;
1127
+ const head = hintedHeader(state.styler, state.theme, state.message, state.hint ?? "(Ctrl+D to finish)");
1128
+ const pointer = state.styler.render(state.theme.roles.pointer, state.theme.icons.pointer);
1129
+ const view = [head, ...[...state.lines.map((line) => state.styler.render(state.theme.roles.content, line)), `${pointer} ${state.styler.render(state.theme.roles.content, state.current)}`]].join("\n");
1130
+ return state.error === void 0 ? view : `${view}\n${errorLine(state.styler, state.theme, state.error)}`;
899
1131
  }
900
1132
  /**
901
1133
  * Advance an editor prompt by one {@link KeyEvent} — the pure `(state, key) → PromptStep<string>`
@@ -929,7 +1161,7 @@ function editorReduce(state, key) {
929
1161
  }
930
1162
  return {
931
1163
  state: clear,
932
- view: `${submitHeader(state.styler, state.message)} ${state.styler.dim(`${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
1164
+ view: `${submitHeader(state.styler, state.theme, state.message)} ${state.styler.render(state.theme.roles.hint, `${String(lines.length)} line${lines.length === 1 ? "" : "s"}`)}`,
933
1165
  status: "submit",
934
1166
  value: answer
935
1167
  };
@@ -979,7 +1211,8 @@ function editLine(value, key) {
979
1211
  * serializes over SSE. Drops everything that cannot cross the wire (a `styler`, and any
980
1212
  * function-valued `validate` rule), while KEEPING the declarative data a remote client needs to
981
1213
  * reconstruct the prompt: the {@link ValidationRules} as data, plus `message` / `choices` /
982
- * `default` / `mask` / `min` / `max`. This is WHY T-a's validation is rules-as-data: a `Validator`
1214
+ * `default` / `mask` / `min` / `max` / `hint` / `theme`. This is WHY T-a's validation is
1215
+ * rules-as-data, and why a theme is data too: a `Validator`
983
1216
  * FUNCTION can't be serialized, but its declarative rules can — the client rebuilds the validator
984
1217
  * from them via {@link resolveValidation}.
985
1218
  *
@@ -1113,6 +1346,13 @@ function sanitizeChoiceLabels(choices) {
1113
1346
  * §14-narrowed, never an `as`; the validator rebuilt from rules via {@link reconstructValidationRules}),
1114
1347
  * then calls the matching prompt form and returns its resolved value.
1115
1348
  *
1349
+ * @remarks
1350
+ * `theme` and `hint` are plain data, so they cross the wire with the rest of the options bag and
1351
+ * are reconstructed here for every form that accepts them: the theme is narrowed by
1352
+ * {@link import('./validators.js').isPromptThemeOptions} (an off-shape theme is dropped, leaving
1353
+ * the defaults), then every glyph and hint is passed through {@link sanitizeDisplayText}. Choice
1354
+ * labels use the stream-safe {@link sanitizeChoiceLabels} treatment instead.
1355
+ *
1116
1356
  * @param terminal - The local {@link PromptFormInterface} to drive
1117
1357
  * @param pending - The decoded pending prompt to dispatch
1118
1358
  * @returns The prompt's resolved value (a `string` / `boolean` / `readonly string[]` per form)
@@ -1121,13 +1361,18 @@ function dispatchPendingPrompt(terminal, pending) {
1121
1361
  const options = pending.options;
1122
1362
  const validate = reconstructValidationRules(options.validate);
1123
1363
  const message = stripControls(pending.message);
1364
+ const supplied = resolveOption(options, "theme", isPromptThemeOptions);
1365
+ const theme = supplied === void 0 ? void 0 : sanitizeThemeIcons(supplied);
1366
+ const raw = resolveOption(options, "hint", isString);
1367
+ const hint = raw === void 0 ? void 0 : sanitizeDisplayText(raw);
1124
1368
  switch (pending.form) {
1125
1369
  case "input": {
1126
1370
  const value = resolveOption(options, "default", isString);
1127
1371
  return terminal.input({
1128
1372
  message,
1129
1373
  ...value !== void 0 ? { default: stripControls(value) } : {},
1130
- ...validate !== void 0 ? { validate } : {}
1374
+ ...validate !== void 0 ? { validate } : {},
1375
+ ...theme !== void 0 ? { theme } : {}
1131
1376
  });
1132
1377
  }
1133
1378
  case "password": {
@@ -1135,14 +1380,17 @@ function dispatchPendingPrompt(terminal, pending) {
1135
1380
  return terminal.password({
1136
1381
  message,
1137
1382
  ...value !== void 0 ? { mask: stripControls(value) } : {},
1138
- ...validate !== void 0 ? { validate } : {}
1383
+ ...validate !== void 0 ? { validate } : {},
1384
+ ...theme !== void 0 ? { theme } : {}
1139
1385
  });
1140
1386
  }
1141
1387
  case "confirm": {
1142
1388
  const value = resolveOption(options, "default", isBoolean);
1143
1389
  return terminal.confirm({
1144
1390
  message,
1145
- ...value !== void 0 ? { default: value } : {}
1391
+ ...value !== void 0 ? { default: value } : {},
1392
+ ...hint !== void 0 ? { hint } : {},
1393
+ ...theme !== void 0 ? { theme } : {}
1146
1394
  });
1147
1395
  }
1148
1396
  case "select": {
@@ -1150,7 +1398,9 @@ function dispatchPendingPrompt(terminal, pending) {
1150
1398
  return terminal.select({
1151
1399
  message,
1152
1400
  choices: sanitizeChoiceLabels(resolveChoices(options, isPromptChoice)),
1153
- ...value !== void 0 ? { default: stripControls(value) } : {}
1401
+ ...value !== void 0 ? { default: stripControls(value) } : {},
1402
+ ...hint !== void 0 ? { hint } : {},
1403
+ ...theme !== void 0 ? { theme } : {}
1154
1404
  });
1155
1405
  }
1156
1406
  case "checkbox": {
@@ -1160,7 +1410,9 @@ function dispatchPendingPrompt(terminal, pending) {
1160
1410
  message,
1161
1411
  choices: sanitizeChoiceLabels(resolveChoices(options, isCheckboxChoice)),
1162
1412
  ...min !== void 0 ? { min } : {},
1163
- ...max !== void 0 ? { max } : {}
1413
+ ...max !== void 0 ? { max } : {},
1414
+ ...hint !== void 0 ? { hint } : {},
1415
+ ...theme !== void 0 ? { theme } : {}
1164
1416
  });
1165
1417
  }
1166
1418
  case "editor": {
@@ -1168,7 +1420,9 @@ function dispatchPendingPrompt(terminal, pending) {
1168
1420
  return terminal.editor({
1169
1421
  message,
1170
1422
  ...value !== void 0 ? { default: stripControls(value) } : {},
1171
- ...validate !== void 0 ? { validate } : {}
1423
+ ...validate !== void 0 ? { validate } : {},
1424
+ ...hint !== void 0 ? { hint } : {},
1425
+ ...theme !== void 0 ? { theme } : {}
1172
1426
  });
1173
1427
  }
1174
1428
  }
@@ -1197,19 +1451,6 @@ function isAbortError(error) {
1197
1451
  return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError";
1198
1452
  }
1199
1453
  /**
1200
- * Parse a JSON wire string TOTAL — a malformed / empty payload yields `undefined` (the caller's
1201
- * guard then rejects it), never a throw. The {@link import('./PromptClient.js').PromptClient}
1202
- * decodes every SSE `data` field through this before §14-narrowing it.
1203
- */
1204
- function parseWireJSON(text) {
1205
- if (text.length === 0) return void 0;
1206
- try {
1207
- return JSON.parse(text);
1208
- } catch {
1209
- return;
1210
- }
1211
- }
1212
- /**
1213
1454
  * Whether `url` is an INSECURE remote endpoint — a plain `http://` URL whose host is NOT a
1214
1455
  * loopback address. Pure string parsing (no `URL` global), so it stays total on malformed input.
1215
1456
  *
@@ -1280,9 +1521,9 @@ function isAnswerPayload(value) {
1280
1521
  * ({@link serializePromptOptions}) so a transport can forward them as-is.
1281
1522
  * - **Answer validates + type-checks.** {@link answer} runs the prompt's per-form gate: it
1282
1523
  * type-checks `value` to the form (string / boolean / string[]) AND, for the text forms, runs
1283
- * the validator resolved from the original `validate` rules. A rejected answer returns `false`
1284
- * and the prompt stays `pending`; an accepted answer resolves the Promise, emits `answer`, and
1285
- * removes the prompt.
1524
+ * the validator resolved from the original `validate` rules. A rejected answer returns a failure
1525
+ * `Result` with error `'rejected'` and the prompt stays `pending`; an accepted answer resolves the
1526
+ * Promise, emits `answer`, and removes the prompt.
1286
1527
  * - **Timeout → expire → reject.** An unanswered prompt expires after `timeout` ms (via the
1287
1528
  * INJECTED timer): `expire` fires and the parked Promise rejects with a {@link TerminalError}
1288
1529
  * (`code: 'EXPIRE'`). {@link destroy} expires every still-pending prompt the same way.
@@ -1631,12 +1872,12 @@ var PromptClient = class {
1631
1872
  }
1632
1873
  async #handle(event) {
1633
1874
  if (event.event === SSE_EVENTS.pending) {
1634
- const parsed = parseWireJSON(event.data);
1875
+ const parsed = parseJSON(event.data);
1635
1876
  if (isPendingPrompt(parsed) && !this.#seen.has(parsed.id)) await this.#dispatch(parsed.id, parsed);
1636
1877
  return;
1637
1878
  }
1638
1879
  if (event.event === SSE_EVENTS.expire) {
1639
- const parsed = parseWireJSON(event.data);
1880
+ const parsed = parseJSON(event.data);
1640
1881
  if (isRecord(parsed) && isString(parsed.id)) {
1641
1882
  this.#seen.delete(parsed.id);
1642
1883
  this.#emitter.emit("expire", parsed.id);
@@ -1714,9 +1955,9 @@ var PromptClient = class {
1714
1955
  * - **Durable open / save.** `open(name)` restores an EMPTY broker from the `store` (parked
1715
1956
  * Promises are process-bound and never resurrected); `save(name)` persists the endpoint's
1716
1957
  * configured `timeout`.
1717
- * - **Removal.** `remove` drops one endpoint or a batch (§9.2, array overload FIRST), destroying
1718
- * each broker (which expires every prompt still parked on it). `clear` drops all; `destroy` is
1719
- * idempotent.
1958
+ * - **Removal.** `remove` drops one endpoint, a batch (§9.2, array overload FIRST), or every
1959
+ * endpoint when called without an argument. It destroys each broker, which expires every prompt
1960
+ * still parked on it. `destroy` is idempotent.
1720
1961
  *
1721
1962
  * @example
1722
1963
  * ```ts
@@ -1855,6 +2096,10 @@ var TerminalManager = class {
1855
2096
  return true;
1856
2097
  }
1857
2098
  remove(names) {
2099
+ if (names === void 0) {
2100
+ for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
2101
+ return;
2102
+ }
1858
2103
  if (isArray(names)) {
1859
2104
  let removed = false;
1860
2105
  for (const name of names) if (this.#removeOne(name)) removed = true;
@@ -1862,13 +2107,10 @@ var TerminalManager = class {
1862
2107
  }
1863
2108
  return this.#removeOne(names);
1864
2109
  }
1865
- clear() {
1866
- for (const name of [...this.#terminals.keys()]) this.#removeOne(name);
1867
- }
1868
2110
  destroy() {
1869
2111
  if (this.#destroyed) return;
1870
2112
  this.#destroyed = true;
1871
- this.clear();
2113
+ this.remove();
1872
2114
  this.#emitter.destroy();
1873
2115
  }
1874
2116
  #createPendingListener() {
@@ -2163,6 +2405,6 @@ function createDatabaseTerminalStore(driver = createMemoryDriver()) {
2163
2405
  }).table("terminals"));
2164
2406
  }
2165
2407
  //#endregion
2166
- export { ACCEPT_EVENT_STREAM, ALPHANUMERIC_PATTERN, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore, EMAIL_PATTERN, ESCAPE, HEADER_TOKEN, INTEGER_PATTERN, KEY_CSI, KEY_SS3, MemoryTerminalStore, NEWLINE, NUMERIC_PATTERN, PROMPT_ICONS, Prompt, PromptClient, RETURN, RULE_MESSAGES, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, TerminalManager, URL_PATTERN, appendRule, buildRuleValidator, checkboxReduce, checkboxView, composeValidators, confirmReduce, confirmView, createCheckboxState, createConfirmState, createDatabaseTerminalStore, createEditorState, createInputState, createMemoryTerminalStore, createPasswordState, createPrompt, createPromptClient, createSelectState, createTerminalManager, defaultTimer, dispatchPendingPrompt, editLine, editorReduce, editorView, errorLine, evaluateRule, gateSelection, globalFetch, inputReduce, inputView, isAbortError, isAnswerPayload, isCheckboxChoice, isInsecureRemote, isPendingPrompt, isPendingPromptStatus, isPrintable, isPromptChoice, isPromptType, isTerminalError, isTerminalSnapshot, normalizeCheckboxChoice, normalizeChoice, parseKey, parseWireJSON, passing, passwordReduce, passwordView, promptHeader, reconstructValidationRules, resolveChoices, resolveOption, resolveValidation, sanitizeChoiceLabels, selectReduce, selectView, serializeChoices, serializeExpire, serializePending, serializePromptOptions, serializeShutdown, serializeValidationRules, submitHeader, toggleIndex };
2408
+ export { ACCEPT_EVENT_STREAM, ALPHANUMERIC_PATTERN, BACKSPACE, CONTROL_NAMES, CTRL_A, CTRL_C, CTRL_D, CTRL_E, CTRL_U, DEFAULT_MASK, DEFAULT_PROMPT_THEME, DEFAULT_PROMPT_TIMEOUT_MS, DEFAULT_RECONNECT_DELAY_MS, DELETE, DatabaseTerminalStore, ESCAPE, HEADER_TOKEN, INTEGER_PATTERN, KEY_CSI, KEY_SS3, MemoryTerminalStore, NEWLINE, NUMERIC_PATTERN, PROMPT_ICONS, PROMPT_ROLES, Prompt, PromptClient, RETURN, RULE_MESSAGES, SEQUENCE_NAMES, SPACE, SSE_BUFFER_LIMIT, SSE_EVENTS, TAB, TerminalError, TerminalManager, URL_PATTERN, appendRule, buildRuleValidator, checkboxReduce, checkboxView, composeValidators, confirmReduce, confirmView, createCheckboxState, createConfirmState, createDatabaseTerminalStore, createEditorState, createInputState, createMemoryTerminalStore, createPasswordState, createPrompt, createPromptClient, createPromptTheme, createSelectState, createTerminalManager, defaultTimer, dispatchPendingPrompt, editLine, editorReduce, editorView, errorLine, evaluateRule, gateSelection, globalFetch, hintedHeader, inputReduce, inputView, isAbortError, isAnswerPayload, isCheckboxChoice, isInsecureRemote, isPendingPrompt, isPendingPromptStatus, isPrintable, isPromptChoice, isPromptThemeOptions, isPromptType, isStyle, isTerminalError, isTerminalSnapshot, normalizeCheckboxChoice, normalizeChoice, parseKey, passing, passwordReduce, passwordView, promptHeader, reconstructValidationRules, resolveChoices, resolveOption, resolveValidation, sanitizeChoiceLabels, sanitizeDisplayText, sanitizeThemeIcons, selectReduce, selectView, serializeChoices, serializeExpire, serializePending, serializePromptOptions, serializeShutdown, serializeValidationRules, submitHeader, toggleIndex };
2167
2409
 
2168
2410
  //# sourceMappingURL=index.js.map