@promptctl/rich-js 0.26.0 → 0.28.0

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.
Files changed (132) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +52 -15
  4. package/dist/core/animation.js +82 -37
  5. package/dist/core/ansi.d.ts +15 -8
  6. package/dist/core/ansi.js +106 -28
  7. package/dist/core/bidi-data.d.ts +14 -0
  8. package/dist/core/bidi-data.js +385 -0
  9. package/dist/core/bidi.d.ts +89 -0
  10. package/dist/core/bidi.js +615 -0
  11. package/dist/core/box.d.ts +9 -1
  12. package/dist/core/box.js +11 -0
  13. package/dist/core/cell-widths.d.ts +3 -2
  14. package/dist/core/cell-widths.js +93 -93
  15. package/dist/core/cells.d.ts +20 -0
  16. package/dist/core/cells.js +93 -5
  17. package/dist/core/color.d.ts +26 -18
  18. package/dist/core/color.js +37 -34
  19. package/dist/core/console.d.ts +26 -1
  20. package/dist/core/console.js +26 -6
  21. package/dist/core/control-codes.d.ts +61 -0
  22. package/dist/core/control-codes.js +157 -0
  23. package/dist/core/easing.js +6 -1
  24. package/dist/core/env.d.ts +6 -0
  25. package/dist/core/env.js +9 -1
  26. package/dist/core/export-html.js +4 -3
  27. package/dist/core/export-lines.d.ts +2 -0
  28. package/dist/core/export-lines.js +7 -3
  29. package/dist/core/export-svg.js +6 -5
  30. package/dist/core/json.js +9 -2
  31. package/dist/core/markup.js +4 -3
  32. package/dist/core/oklch.d.ts +2 -0
  33. package/dist/core/oklch.js +3 -3
  34. package/dist/core/orient.d.ts +68 -0
  35. package/dist/core/orient.js +224 -0
  36. package/dist/core/osc8.d.ts +12 -5
  37. package/dist/core/osc8.js +14 -7
  38. package/dist/core/pretty.js +22 -18
  39. package/dist/core/protocol.d.ts +53 -22
  40. package/dist/core/protocol.js +14 -6
  41. package/dist/core/render.d.ts +8 -3
  42. package/dist/core/render.js +19 -5
  43. package/dist/core/seal.d.ts +26 -0
  44. package/dist/core/seal.js +75 -0
  45. package/dist/core/segment.d.ts +9 -1
  46. package/dist/core/segment.js +11 -1
  47. package/dist/core/style.d.ts +29 -1
  48. package/dist/core/style.js +41 -5
  49. package/dist/core/text-order.d.ts +51 -0
  50. package/dist/core/text-order.js +50 -0
  51. package/dist/core/text.d.ts +10 -0
  52. package/dist/core/text.js +133 -87
  53. package/dist/core/timeline.js +6 -6
  54. package/dist/core/view.d.ts +23 -9
  55. package/dist/core/view.js +37 -12
  56. package/dist/core/wrap.d.ts +24 -6
  57. package/dist/core/wrap.js +82 -16
  58. package/dist/host/app.d.ts +48 -7
  59. package/dist/host/app.js +116 -11
  60. package/dist/host/index.d.ts +3 -2
  61. package/dist/host/index.js +1 -0
  62. package/dist/host/input.d.ts +35 -2
  63. package/dist/host/input.js +88 -4
  64. package/dist/host/terminal-colors.d.ts +18 -0
  65. package/dist/host/terminal-colors.js +46 -0
  66. package/dist/host/terminal-host.d.ts +2 -1
  67. package/dist/index.d.ts +10 -8
  68. package/dist/index.js +2 -2
  69. package/dist/renderables/change.d.ts +1 -3
  70. package/dist/renderables/change.js +4 -6
  71. package/dist/renderables/effect.d.ts +61 -60
  72. package/dist/renderables/effect.js +91 -81
  73. package/dist/renderables/effects.d.ts +37 -8
  74. package/dist/renderables/effects.js +193 -55
  75. package/dist/renderables/frames.d.ts +1 -10
  76. package/dist/renderables/frames.js +1 -16
  77. package/dist/renderables/glyph-effect.d.ts +12 -13
  78. package/dist/renderables/glyph-effect.js +34 -32
  79. package/dist/renderables/glyph-effects.d.ts +27 -8
  80. package/dist/renderables/glyph-effects.js +75 -7
  81. package/dist/renderables/lifecycle.d.ts +40 -32
  82. package/dist/renderables/lifecycle.js +87 -48
  83. package/dist/renderables/live.d.ts +3 -3
  84. package/dist/renderables/live.js +4 -4
  85. package/dist/renderables/markdown.js +4 -2
  86. package/dist/renderables/panel.js +6 -4
  87. package/dist/renderables/progress.d.ts +11 -4
  88. package/dist/renderables/progress.js +29 -17
  89. package/dist/renderables/progressBar.d.ts +3 -3
  90. package/dist/renderables/progressBar.js +16 -13
  91. package/dist/renderables/syntax.js +7 -2
  92. package/dist/renderables/table.js +2 -1
  93. package/dist/template-bindings/animation-funcs.d.ts +49 -18
  94. package/dist/template-bindings/animation-funcs.js +115 -32
  95. package/dist/template-bindings/index.d.ts +7 -5
  96. package/dist/template-bindings/index.js +8 -5
  97. package/dist/template-bindings/palette-funcs.d.ts +1 -1
  98. package/dist/template-bindings/palette-funcs.js +1 -1
  99. package/dist/themes/buildPalette.d.ts +15 -39
  100. package/dist/themes/buildPalette.js +196 -67
  101. package/dist/themes/colorMath.d.ts +10 -4
  102. package/dist/themes/colorMath.js +33 -1
  103. package/dist/themes/data/index.js +1 -4
  104. package/dist/themes/data/textual-ansi.js +0 -143
  105. package/dist/themes/data/types.d.ts +3 -1
  106. package/dist/themes/palette.d.ts +22 -6
  107. package/dist/themes/palette.js +13 -21
  108. package/dist/themes/ramp.d.ts +1 -1
  109. package/dist/themes/ramp.js +1 -1
  110. package/dist/themes/registry.d.ts +7 -10
  111. package/dist/themes/registry.js +9 -30
  112. package/dist/themes/terminalThemes.d.ts +4 -10
  113. package/dist/themes/terminalThemes.js +31 -46
  114. package/dist/themes/transpose.d.ts +5 -10
  115. package/dist/themes/transpose.js +13 -24
  116. package/dist/widgets/button.d.ts +1 -1
  117. package/dist/widgets/button.js +6 -6
  118. package/dist/widgets/checkbox.js +4 -4
  119. package/dist/widgets/dropdown.d.ts +1 -1
  120. package/dist/widgets/dropdown.js +7 -5
  121. package/dist/widgets/event-router.js +2 -2
  122. package/dist/widgets/ink.d.ts +5 -4
  123. package/dist/widgets/ink.js +6 -9
  124. package/dist/widgets/text-input.d.ts +1 -1
  125. package/dist/widgets/text-input.js +41 -32
  126. package/dist/widgets/themed-widget.js +3 -3
  127. package/dist/widgets/toggle.d.ts +1 -1
  128. package/dist/widgets/toggle.js +5 -5
  129. package/dist/widgets/types.d.ts +2 -2
  130. package/dist/widgets/widget-app.d.ts +3 -3
  131. package/dist/widgets/widget-app.js +5 -5
  132. package/package.json +5 -4
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Terminal color representation, parsing, downgrading, and ANSI code generation.
3
3
  */
4
+ import { isDumbTerminal } from "./env.js";
4
5
  import { Memo, WeakMemo } from "./memo.js";
5
6
  import { pattern } from "./pattern.js";
6
- // Runtime-importing `Palette` from `themes/palette.ts` is safe: that module
7
- // only `import type`s from `core/color.ts`, so there is no runtime cycle.
8
- import { Palette } from "../themes/palette.js";
7
+ import { detectTextOrder } from "./text-order.js";
9
8
  // --- ColorRgba ---
10
9
  function hex2(byte) {
11
10
  return byte.toString(16).padStart(2, "0");
@@ -320,23 +319,15 @@ function forcedColorSystem(env) {
320
319
  function envOf(options) {
321
320
  return options.env ?? (typeof process !== "undefined" ? process.env : {});
322
321
  }
323
- /**
324
- * A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
325
- * as Emacs' shell mode sets it. It takes no colour and nothing that moves the
326
- * cursor.
327
- */
328
- export function isDumbTerminal(env) {
329
- const term = env["TERM"];
330
- return term === "dumb" || term === "unknown";
322
+ function isTTYOf(options) {
323
+ return options.isTTY ?? (typeof process !== "undefined" ? (process.stdout?.isTTY ?? false) : false);
331
324
  }
332
325
  // What the destination can draw, preferences aside: TTY presence,
333
326
  // TERM=dumb/unknown, COLORTERM, known terminal names, TERM patterns. `null`
334
327
  // here means the destination takes no escapes at all.
335
328
  function terminalColorSystem(options) {
336
329
  const env = envOf(options);
337
- const isTTY = options.isTTY ??
338
- (typeof process !== "undefined" ? (process.stdout?.isTTY ?? false) : false);
339
- if (!isTTY)
330
+ if (!isTTYOf(options))
340
331
  return null;
341
332
  if (isDumbTerminal(env))
342
333
  return null;
@@ -379,11 +370,14 @@ function terminalColorSystem(options) {
379
370
  */
380
371
  export function resolveDestination(spec, options = {}) {
381
372
  const reducedMotion = options.reducedMotion ?? false;
373
+ // Ahead of both arms: a colour spec says nothing about order.
374
+ const textOrder = options.textOrder ?? detectTextOrder(envOf(options), isTTYOf(options));
382
375
  if (spec !== "auto") {
383
376
  return {
384
377
  colorSystem: typeof spec === "string" ? resolveColorSystem(spec) : spec,
385
378
  hyperlinks: true,
386
379
  reducedMotion,
380
+ textOrder,
387
381
  };
388
382
  }
389
383
  return {
@@ -391,6 +385,7 @@ export function resolveDestination(spec, options = {}) {
391
385
  hyperlinks: (forcedColorSystem(envOf(options)) ?? null) !== null ||
392
386
  terminalColorSystem(options) !== null,
393
387
  reducedMotion,
388
+ textOrder,
394
389
  };
395
390
  }
396
391
  /**
@@ -844,24 +839,17 @@ export function blendRgb(color1, color2, crossFade = 0.5) {
844
839
  }
845
840
  // --- TerminalTheme ---
846
841
  /**
847
- * A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
848
- * and a semantic palette.
849
- *
850
- * **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
851
- * showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
852
- * is ANSI colour 1 under every theme; which red that is, is the theme's to say,
853
- * as it is in Rich, where each `TerminalTheme` carries its own table.
842
+ * What a terminal draws with: the part of a `TerminalTheme` a colour resolves
843
+ * against.
854
844
  */
855
- export class TerminalTheme {
845
+ export class TerminalColors {
856
846
  backgroundColor;
857
847
  foregroundColor;
858
848
  ansiColors;
859
- palette;
860
- constructor(backgroundColor, foregroundColor, ansiColors, palette) {
849
+ constructor(backgroundColor, foregroundColor, ansiColors) {
861
850
  this.backgroundColor = backgroundColor;
862
851
  this.foregroundColor = foregroundColor;
863
852
  this.ansiColors = ansiColors;
864
- this.palette = palette;
865
853
  // [LAW:parse-dont-validate] Every reader indexes `ansiColors` 0–15 as the
866
854
  // terminal's sixteen, so a theme is the proof it holds exactly those.
867
855
  if (ansiColors.firstIndex !== 0 || ansiColors.size !== 16) {
@@ -869,6 +857,22 @@ export class TerminalTheme {
869
857
  }
870
858
  }
871
859
  }
860
+ /**
861
+ * A terminal theme — surface/foreground baseline, the sixteen ANSI colours,
862
+ * and a semantic palette.
863
+ *
864
+ * **`ansiColors` is the theme's own sixteen colours**, the shades a terminal
865
+ * showing this theme draws `red`, `blue` and the rest in. `ColorSpec.parse("red")`
866
+ * is ANSI colour 1 under every theme; which red that is, is the theme's to say,
867
+ * as it is in Rich, where each `TerminalTheme` carries its own table.
868
+ */
869
+ export class TerminalTheme extends TerminalColors {
870
+ palette;
871
+ constructor(backgroundColor, foregroundColor, ansiColors, palette) {
872
+ super(backgroundColor, foregroundColor, ansiColors);
873
+ this.palette = palette;
874
+ }
875
+ }
872
876
  // --- ColorTable data ---
873
877
  function buildStandard16() {
874
878
  return [
@@ -943,20 +947,19 @@ export const EIGHT_BIT_TABLE = new ColorTable(build256Table());
943
947
  */
944
948
  export const EIGHT_BIT_DOWNGRADE_TABLE = new ColorTable(build256Table().slice(16), 16);
945
949
  export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
946
- // --- Internal fallback theme ---
950
+ // --- Internal fallback terminals ---
947
951
  //
948
952
  // [LAW:one-way-deps] Preset themes (DEFAULT_TERMINAL_THEME, MONOKAI, NORD, ...)
949
- // live in `src/themes/terminalThemes.ts` because they need `buildPalette` from
950
- // `themes/`. Keeping them out of `core/color.ts` makes the dependency strictly
953
+ // live in `src/themes/terminalThemes.ts` because they hold the registry's
954
+ // palettes. Keeping them out of `core/color.ts` makes the dependency strictly
951
955
  // `themes/* -> core/color`, with no runtime back-edge.
952
956
  //
953
957
  // `getTruecolor()` retains a fallback for callers that omit `theme`: the
954
- // terminal a colour's depth assumes, built from that depth's own sixteen with
955
- // an empty Palette — sufficient for the lookups it serves, which never read
956
- // `palette`.
957
- const INTERNAL_DEFAULT_THEME = new TerminalTheme(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE, new Palette("default", true, new Map()));
958
+ // terminal a colour's depth assumes, its own sixteen and no palette, which the
959
+ // lookups it serves never read.
960
+ const INTERNAL_DEFAULT_TERMINAL = new TerminalColors(new ColorRgba(0, 0, 0), new ColorRgba(255, 255, 255), STANDARD_TABLE);
958
961
  /** The Windows console in its default Campbell scheme: a WINDOWS colour's terminal. */
959
- const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE, new Palette("windows", true, new Map()));
962
+ const WINDOWS_CONSOLE_TERMINAL = new TerminalColors(WINDOWS_TABLE.get(0), WINDOWS_TABLE.get(7), WINDOWS_TABLE);
960
963
  /**
961
964
  * The terminal a colour at `depth` is drawn by: `theme`, or when a caller names
962
965
  * none, the one the depth assumes — the Windows console at WINDOWS, the VGA
@@ -965,7 +968,7 @@ const WINDOWS_CONSOLE_THEME = new TerminalTheme(WINDOWS_TABLE.get(0), WINDOWS_TA
965
968
  * palette this does not.)
966
969
  */
967
970
  export function resolveTerminal(theme, depth) {
968
- return theme ?? (depth === ColorDepth.WINDOWS ? WINDOWS_CONSOLE_THEME : INTERNAL_DEFAULT_THEME);
971
+ return theme ?? (depth === ColorDepth.WINDOWS ? WINDOWS_CONSOLE_TERMINAL : INTERNAL_DEFAULT_TERMINAL);
969
972
  }
970
973
  /**
971
974
  * The table a downgrade to `depth` rounds a fixed colour against.
@@ -3,13 +3,15 @@
3
3
  */
4
4
  import { Style, Theme } from "./style.js";
5
5
  import { ColorDepth } from "./color.js";
6
+ import type { TextOrder } from "./text-order.js";
6
7
  import type { ColorSystemName, Destination } from "./color.js";
7
8
  import type { Env } from "./env.js";
8
- import type { TerminalTheme } from "./color.js";
9
+ import type { TerminalColors, TerminalTheme } from "./color.js";
9
10
  import { type JSONOptions } from "./json.js";
10
11
  import type { Highlighter } from "./highlighter.js";
11
12
  import { type RuleOptions } from "../renderables/rule.js";
12
13
  import type { Height, OverflowMethod, RenderOptions, StyleErrorHandler } from "./protocol.js";
14
+ import { type HostFrame } from "./protocol.js";
13
15
  import type { Unsubscribe } from "./subscription.js";
14
16
  export interface ConsoleSink {
15
17
  write(chunk: string | Uint8Array): unknown;
@@ -52,6 +54,8 @@ export interface ConsoleOptions {
52
54
  * TERM=dumb: false). `false` with `colorSystem: null` writes plain text.
53
55
  */
54
56
  hyperlinks?: boolean;
57
+ /** What the output's destination does with direction. Default: what `detectTextOrder` finds. */
58
+ textOrder?: TextOrder;
55
59
  /**
56
60
  * The terminal can draw only ASCII: boxes, rules, tree guides and widget
57
61
  * marks draw with ASCII characters instead of line-drawing glyphs. A
@@ -68,6 +72,12 @@ export interface ConsoleOptions {
68
72
  * it in; nothing here reads the environment for it. Default false.
69
73
  */
70
74
  reducedMotion?: boolean;
75
+ /**
76
+ * The terminal this console draws on (`RenderOptions.terminal`), which
77
+ * every effect it draws resolves its colours on. Default: the one the
78
+ * colour depth assumes.
79
+ */
80
+ terminal?: TerminalColors;
71
81
  /**
72
82
  * Static width (cells). Ignored when `getSize` is provided. Falls back to
73
83
  * the `COLUMNS` env var / the bound stream's `columns` / 80.
@@ -125,6 +135,13 @@ export interface ConsoleOptions {
125
135
  }
126
136
  export interface PrintOptions {
127
137
  style?: string | Style;
138
+ /**
139
+ * The frame this print is drawn in (`RenderOptions.frame`): the moment
140
+ * everything in it that moves is drawn for, named by the caller — a host's
141
+ * `frameNow()`. [LAW:single-enforcer] The one place a print's time is
142
+ * decided; absent, the print is drawn for no moment, 0 (`frameTimeOf`).
143
+ */
144
+ frame?: HostFrame;
128
145
  justify?: "default" | "left" | "center" | "right" | "full";
129
146
  overflow?: OverflowMethod;
130
147
  highlight?: boolean;
@@ -141,6 +158,7 @@ export declare class Console {
141
158
  private readonly _lastLogTime;
142
159
  private readonly _style;
143
160
  private readonly _isTerminal;
161
+ private readonly _attended;
144
162
  private readonly _interactive;
145
163
  private readonly _file;
146
164
  private readonly _stream;
@@ -148,6 +166,7 @@ export declare class Console {
148
166
  private readonly _markup;
149
167
  private readonly _highlight;
150
168
  private readonly _theme;
169
+ private readonly _terminal;
151
170
  private readonly _onStyleError;
152
171
  private readonly _asciiOnly;
153
172
  private readonly _highlighter;
@@ -180,6 +199,12 @@ export declare class Console {
180
199
  print(...args: unknown[]): void;
181
200
  private _draw;
182
201
  log(...args: unknown[]): void;
202
+ /**
203
+ * Sounds the terminal's bell, Rich's `Console.bell`, for whoever is at it: a
204
+ * pipe, a dumb terminal and a capture get nothing. It goes straight to the sink
205
+ * because a bell moves no cursor, and a live region would repaint its frame.
206
+ */
207
+ bell(): void;
183
208
  rule(title?: string, options?: RuleOptions): void;
184
209
  printJson(json: string | object, options?: JSONOptions): void;
185
210
  /**
@@ -3,7 +3,8 @@
3
3
  */
4
4
  import { Segment } from "./segment.js";
5
5
  import { NULL_STYLE, DEFAULT_THEME } from "./style.js";
6
- import { isDumbTerminal, resolveDestination } from "./color.js";
6
+ import { resolveDestination } from "./color.js";
7
+ import { isDumbTerminal } from "./env.js";
7
8
  import { encodeHtml } from "./export-html.js";
8
9
  import { encodeSvg } from "./export-svg.js";
9
10
  import { RichText } from "./text.js";
@@ -17,6 +18,7 @@ import { ReprHighlighter, NullHighlighter } from "./highlighter.js";
17
18
  // rather than being restated here; see `rule()`.
18
19
  import { Rule } from "../renderables/rule.js";
19
20
  import { segmentsToString, shownOn } from "./render.js";
21
+ import { sealed } from "./seal.js";
20
22
  import { placeBlock } from "./place.js";
21
23
  import { activePass, isRenderable, measure, render } from "./protocol.js";
22
24
  import { ListState, State, refuseRenderWrite } from "./state.js";
@@ -181,6 +183,7 @@ export class Console {
181
183
  _lastLogTime = new State(null);
182
184
  _style;
183
185
  _isTerminal;
186
+ _attended;
184
187
  _interactive;
185
188
  _file;
186
189
  // [LAW:one-source-of-truth] The resolved output stream, not the `stderr`
@@ -191,6 +194,7 @@ export class Console {
191
194
  _markup;
192
195
  _highlight;
193
196
  _theme;
197
+ _terminal;
194
198
  _onStyleError;
195
199
  _asciiOnly;
196
200
  _highlighter;
@@ -211,7 +215,7 @@ export class Console {
211
215
  // [LAW:dataflow-not-control-flow] `isTTY` and `env` are forwarded
212
216
  // unconditionally, so detection stays inside the console's injected
213
217
  // environment rather than the ambient process's.
214
- const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env, reducedMotion: options?.reducedMotion });
218
+ const resolved = resolveDestination(options?.colorSystem === undefined ? "auto" : options.colorSystem, { isTTY: this._isTerminal, env: environment.env, reducedMotion: options?.reducedMotion, ...(options?.textOrder === undefined ? {} : { textOrder: options.textOrder }) });
215
219
  // [LAW:single-enforcer] The console's one override, an explicit
216
220
  // `hyperlinks`, is applied here and nowhere else; every write encodes for
217
221
  // this value whole. [LAW:one-source-of-truth] The reader's motion
@@ -223,9 +227,12 @@ export class Console {
223
227
  // [LAW:no-ambient-temporal-coupling] The theme is assigned before the
224
228
  // style, because the console's own style may be one of the theme's names.
225
229
  this._theme = options?.theme ?? DEFAULT_THEME;
230
+ this._terminal = options?.terminal;
226
231
  this._style = this._theme.resolve(options?.style ?? NULL_STYLE);
227
- // Rich's rule: a terminal is interactive unless it is a dumb one.
228
- this._interactive = options?.forceInteractive ?? (this._isTerminal && !isDumbTerminal(environment.env));
232
+ // Rich's rule: a terminal is interactive unless it is a dumb one. Forcing that
233
+ // is animation policy, and says nothing of who is at the terminal.
234
+ this._attended = this._isTerminal && !isDumbTerminal(environment.env);
235
+ this._interactive = options?.forceInteractive ?? this._attended;
229
236
  this._file = options?.file;
230
237
  this._record = options?.record ?? false;
231
238
  this._markup = options?.markup !== false;
@@ -290,6 +297,8 @@ export class Console {
290
297
  theme: this._theme,
291
298
  onStyleError: this._onStyleError,
292
299
  colorSystem: this._destination.colorSystem,
300
+ textOrder: this._destination.textOrder,
301
+ terminal: this._terminal,
293
302
  markup: this._markup,
294
303
  highlight: this._highlight,
295
304
  highlighter: this._highlighter,
@@ -317,7 +326,7 @@ export class Console {
317
326
  !isRenderable(lastArg) &&
318
327
  ("style" in lastArg || "justify" in lastArg || "markup" in lastArg ||
319
328
  "highlight" in lastArg || "overflow" in lastArg || "end" in lastArg ||
320
- "softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg)) {
329
+ "softWrap" in lastArg || "crop" in lastArg || "sep" in lastArg || "frame" in lastArg)) {
321
330
  opts = lastArg;
322
331
  items = args.slice(0, -1);
323
332
  }
@@ -407,6 +416,7 @@ export class Console {
407
416
  const renderOpts = {
408
417
  ...options,
409
418
  ...strings,
419
+ frame: opts.frame,
410
420
  justify: opts.justify === "default" ? undefined : opts.justify,
411
421
  overflow: opts.overflow ?? (softWrap ? "ignore" : undefined),
412
422
  noWrap: softWrap,
@@ -494,6 +504,16 @@ export class Console {
494
504
  // stamp, so the next row's must show.
495
505
  this._lastLogTime.set(stamped);
496
506
  }
507
+ /**
508
+ * Sounds the terminal's bell, Rich's `Console.bell`, for whoever is at it: a
509
+ * pipe, a dumb terminal and a capture get nothing. It goes straight to the sink
510
+ * because a bell moves no cursor, and a live region would repaint its frame.
511
+ */
512
+ bell() {
513
+ refuseRenderWrite(this._madeIn);
514
+ if (this._attended && this._capture.get() === null)
515
+ this.file.write("\x07");
516
+ }
497
517
  // [LAW:one-source-of-truth] `RuleOptions` is `Rule`'s, not a restatement of
498
518
  // it. `console.ts` used to declare its own copy and field-by-field forward
499
519
  // into this constructor, which made a second map of one shape — identical
@@ -546,7 +566,7 @@ export class Console {
546
566
  }
547
567
  // --- Export (when record:true) ---
548
568
  exportText({ clear = true } = {}) {
549
- const text = this._recorded.get().map((s) => s.text).join("");
569
+ const text = [...sealed(this._recorded.get(), "plain")].map((s) => s.text).join("");
550
570
  if (clear)
551
571
  this._recorded.set([]);
552
572
  return text;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The census of code points that do not print: every one in Unicode's general
3
+ * categories Cc, Cf, Zl and Zp, and the variation selectors, with what `RichText`
4
+ * does when content carries it. [LAW:one-source-of-truth] The sanitizer
5
+ * (`sanitized`), the wire (`wireText`) and the cell-width measure
6
+ * (`cellLen`) all read this table, so whether content keeps a code point has one
7
+ * statement. `docs/control-codes.md` is each family's use and harm, and what real
8
+ * terminals were measured doing with it.
9
+ *
10
+ * A row does not carry a width. A kept code point takes the cells Rich's table
11
+ * gives it (`cell-widths.ts`), and a second statement of them here would be one
12
+ * that could drift.
13
+ */
14
+ /**
15
+ * What `RichText` does with a code point. `strip` deletes it from content. `keep`
16
+ * holds it as a character. `line-break` is a line ending, so content holds an LF
17
+ * in its place. `pictured` means nothing inside a span, so content holds the
18
+ * stand-in `pictureOf` gives in its place and the reader sees something was there.
19
+ * `withheld` is held in content and measured as nothing, and `wireText` decides
20
+ * from its neighbours whether it is written.
21
+ *
22
+ * Only `keep`, and a `withheld` code point `wireText` lets by, reaches the wire. A
23
+ * raw `Segment` is not content, so what it holds of the others is deleted there.
24
+ */
25
+ export type Disposition = "strip" | "keep" | "line-break" | "pictured" | "withheld";
26
+ /**
27
+ * The code points `first` to `last`, inclusive, and what is done with each. A
28
+ * `pictured` row carries what content shows in place of each of its code points.
29
+ */
30
+ export type ControlCodeRow = {
31
+ readonly first: number;
32
+ readonly last: number;
33
+ readonly disposition: Exclude<Disposition, "pictured">;
34
+ } | {
35
+ readonly first: number;
36
+ readonly last: number;
37
+ readonly disposition: "pictured";
38
+ readonly picture: (codePoint: number) => string;
39
+ };
40
+ /**
41
+ * Ascending and disjoint, every Cc, Cf, Zl, Zp and Variation_Selector code point
42
+ * in exactly one row (`test/core/control-codes.test.ts`).
43
+ *
44
+ * Rich's `Text` strips only `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR) and writes
45
+ * every other code point to the wire. Here a directive reaches the wire only
46
+ * through the library's own constructs: `Style`, a link, `Console.bell`, the paint
47
+ * layer.
48
+ */
49
+ export declare const CONTROL_CODES: readonly ControlCodeRow[];
50
+ /** What content shows in place of `codePoint`, whose disposition is `pictured`. */
51
+ export declare function pictureOf(codePoint: number): string;
52
+ /** A regular expression class of the code points in rows `chosen` picks, for `u`. */
53
+ export declare function controlClass(chosen: (disposition: Disposition) => boolean): string;
54
+ /**
55
+ * UTS #51's subdivision flag, as a `u` source: the black flag, a CLDR subdivision
56
+ * spelled in tag letters and digits, and CANCEL TAG. The one sequence tag
57
+ * characters have a reader for.
58
+ */
59
+ export declare const SUBDIVISION_FLAG = "\uD83C\uDFF4[\uDB40\uDC30-\uDB40\uDC39\uDB40\uDC61-\uDB40\uDC7A]{3,6}\uDB40\uDC7F";
60
+ /** What happens to `codePoint`. One in no row is kept. */
61
+ export declare function dispositionOf(codePoint: number): Disposition;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The census of code points that do not print: every one in Unicode's general
3
+ * categories Cc, Cf, Zl and Zp, and the variation selectors, with what `RichText`
4
+ * does when content carries it. [LAW:one-source-of-truth] The sanitizer
5
+ * (`sanitized`), the wire (`wireText`) and the cell-width measure
6
+ * (`cellLen`) all read this table, so whether content keeps a code point has one
7
+ * statement. `docs/control-codes.md` is each family's use and harm, and what real
8
+ * terminals were measured doing with it.
9
+ *
10
+ * A row does not carry a width. A kept code point takes the cells Rich's table
11
+ * gives it (`cell-widths.ts`), and a second statement of them here would be one
12
+ * that could drift.
13
+ */
14
+ const strip = (first, last) => ({ first, last, disposition: "strip" });
15
+ const keep = (first, last) => ({ first, last, disposition: "keep" });
16
+ const lineBreak = (first, last) => ({ first, last, disposition: "line-break" });
17
+ const pictured = (first, last, picture) => ({ first, last, disposition: "pictured", picture });
18
+ /**
19
+ * The stand-ins. A C0 control's Control Pictures symbol is U+2400 up, DEL's is
20
+ * U+2421. Unicode gives C1 no pictures, so each shows U+2426 SYMBOL FOR SUBSTITUTE:
21
+ * one cell under every East Asian width setting, which U+FFFD is not, and apart
22
+ * from a real decoding error. Each is one code unit, so offsets hold.
23
+ */
24
+ const controlPicture = (codePoint) => String.fromCharCode(0x2400 + codePoint);
25
+ const deletePicture = () => "\u2421";
26
+ const substitute = () => "\u2426";
27
+ const withheld = (first, last) => ({ first, last, disposition: "withheld" });
28
+ /**
29
+ * Ascending and disjoint, every Cc, Cf, Zl, Zp and Variation_Selector code point
30
+ * in exactly one row (`test/core/control-codes.test.ts`).
31
+ *
32
+ * Rich's `Text` strips only `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR) and writes
33
+ * every other code point to the wire. Here a directive reaches the wire only
34
+ * through the library's own constructs: `Style`, a link, `Console.bell`, the paint
35
+ * layer.
36
+ */
37
+ export const CONTROL_CODES = [
38
+ // NUL to BS. BEL's alert is `Console.bell`, and BS's overstrike, bold and
39
+ // underline in nroff output, is `decodeAnsi`: taken a code point at a time, text
40
+ // can turn neither into what it means.
41
+ pictured(0x00, 0x08, controlPicture),
42
+ keep(0x09, 0x0a),
43
+ // VT and FF, mandatory breaks in UAX #14 as LS and PS are.
44
+ lineBreak(0x0b, 0x0c),
45
+ // CR. A CRLF's LF ends its line however the two were chunked, and a CR's
46
+ // overwrite is `decodeAnsi`'s. What is given up is a classic Mac file's lines.
47
+ strip(0x0d, 0x0d),
48
+ // SO and SI switch the terminal's character set; DLE to SUB are transmission
49
+ // controls, and CAN and SUB abort a sequence; ESC begins one, and content that
50
+ // means styling says so with `decodeAnsi`; FS to US are data separators.
51
+ pictured(0x0e, 0x1f, controlPicture),
52
+ // DEL.
53
+ pictured(0x7f, 0x7f, deletePicture),
54
+ // The C1 controls. VTE, WezTerm and xterm.js read U+009B as CSI and U+009D as
55
+ // OSC; xterm, kitty and others ignore them. None has a meaning in text but NEL,
56
+ // which Unicode treats as CR, LF and CRLF are treated (§5.8). A stray one is
57
+ // often windows-1252 decoded as Latin-1, but which, a code point alone cannot
58
+ // say, so mojibake is the caller's to repair. `TextInput` refuses them as keys.
59
+ pictured(0x80, 0x84, substitute),
60
+ lineBreak(0x85, 0x85),
61
+ pictured(0x86, 0x9f, substitute),
62
+ // SOFT HYPHEN: a place a word may break, drawn as a hyphen only where one does.
63
+ // Terminals disagree on its cell, so it never reaches them.
64
+ withheld(0xad, 0xad),
65
+ // The Arabic and Syriac number signs and other prepended concatenation marks,
66
+ // which draw a glyph.
67
+ keep(0x600, 0x605),
68
+ // ARABIC LETTER MARK, a bidi mark like LRM and RLM below: it holds no state,
69
+ // so right-to-left text can use it.
70
+ keep(0x61c, 0x61c),
71
+ // More prepended concatenation marks.
72
+ keep(0x6dd, 0x6dd),
73
+ keep(0x70f, 0x70f),
74
+ keep(0x890, 0x891),
75
+ keep(0x8e2, 0x8e2),
76
+ // The Mongolian free variation selectors.
77
+ withheld(0x180b, 0x180d),
78
+ // MONGOLIAN VOWEL SEPARATOR, which shapes the vowel after it.
79
+ keep(0x180e, 0x180e),
80
+ withheld(0x180f, 0x180f),
81
+ // ZWSP, a place a line may break (`wrap.ts`); ZWNJ, which Persian and Urdu spell
82
+ // with; ZWJ, for emoji and conjuncts; and the bidi marks LRM and RLM, which hold
83
+ // no state, so right-to-left text can use them. ZWJ's joining is Rich's width
84
+ // rule, which `cells.ts` holds apart from this row.
85
+ keep(0x200b, 0x200f),
86
+ // LINE SEPARATOR and PARAGRAPH SEPARATOR.
87
+ lineBreak(0x2028, 0x2029),
88
+ // The bidi embeddings and overrides with their closer PDF, and the isolates
89
+ // below. Content holds them; what reaches the wire is `sealed`'s to decide.
90
+ keep(0x202a, 0x202e),
91
+ // WORD JOINER, which only asks that nothing break here, and the invisible
92
+ // mathematical operators.
93
+ keep(0x2060, 0x2064),
94
+ // The bidi isolates with their closer PDI.
95
+ keep(0x2066, 0x2069),
96
+ // The deprecated shaping and swapping switches. Each holds state to the end of
97
+ // the text, Unicode deprecates them, and no terminal was found honouring one.
98
+ strip(0x206a, 0x206f),
99
+ // VARIATION SELECTOR-1 to 16: glyph variants, and text or emoji presentation.
100
+ withheld(0xfe00, 0xfe0f),
101
+ // ZERO WIDTH NO-BREAK SPACE, the byte order mark, as a word joiner.
102
+ keep(0xfeff, 0xfeff),
103
+ // Interlinear annotation. No terminal draws ruby, the annotating text stays
104
+ // visible, and the anchors hold no state, so there is nothing to contain.
105
+ keep(0xfff9, 0xfffb),
106
+ // The Kaithi number signs, which draw a glyph.
107
+ keep(0x110bd, 0x110bd),
108
+ keep(0x110cd, 0x110cd),
109
+ // Egyptian hieroglyph, shorthand and musical format controls.
110
+ keep(0x13430, 0x1343f),
111
+ keep(0x1bca0, 0x1bca3),
112
+ keep(0x1d173, 0x1d17a),
113
+ // LANGUAGE TAG, deprecated.
114
+ strip(0xe0001, 0xe0001),
115
+ // The tag characters: an emoji subdivision flag, or text no reader can see.
116
+ withheld(0xe0020, 0xe007f),
117
+ // The ideographic variation selectors: Japanese and Chinese glyph variants.
118
+ withheld(0xe0100, 0xe01ef),
119
+ ];
120
+ /** What content shows in place of `codePoint`, whose disposition is `pictured`. */
121
+ export function pictureOf(codePoint) {
122
+ const row = rowOf(codePoint);
123
+ if (row?.disposition !== "pictured")
124
+ throw new RangeError(`U+${codePoint.toString(16).padStart(4, "0")} is not pictured`);
125
+ return row.picture(codePoint);
126
+ }
127
+ /** A regular expression class of the code points in rows `chosen` picks, for `u`. */
128
+ export function controlClass(chosen) {
129
+ const hex = (codePoint) => `\\u{${codePoint.toString(16)}}`;
130
+ return `[${CONTROL_CODES.filter((row) => chosen(row.disposition)).map((row) => `${hex(row.first)}-${hex(row.last)}`).join("")}]`;
131
+ }
132
+ /**
133
+ * UTS #51's subdivision flag, as a `u` source: the black flag, a CLDR subdivision
134
+ * spelled in tag letters and digits, and CANCEL TAG. The one sequence tag
135
+ * characters have a reader for.
136
+ */
137
+ export const SUBDIVISION_FLAG = "\u{1f3f4}[\u{e0030}-\u{e0039}\u{e0061}-\u{e007a}]{3,6}\u{e007f}";
138
+ /** What happens to `codePoint`. One in no row is kept. */
139
+ export function dispositionOf(codePoint) {
140
+ return rowOf(codePoint)?.disposition ?? "keep";
141
+ }
142
+ /** The row holding `codePoint`, if any. */
143
+ function rowOf(codePoint) {
144
+ let low = 0;
145
+ let high = CONTROL_CODES.length - 1;
146
+ while (low <= high) {
147
+ const mid = (low + high) >> 1;
148
+ const row = CONTROL_CODES[mid];
149
+ if (codePoint < row.first)
150
+ high = mid - 1;
151
+ else if (codePoint > row.last)
152
+ low = mid + 1;
153
+ else
154
+ return row;
155
+ }
156
+ return undefined;
157
+ }
@@ -379,7 +379,12 @@ export function passAt(what, t, period) {
379
379
  // takes off; carrying a pass's start out of `t` itself would leave each
380
380
  // sample its own rounding. `+ 0` turns the `-0` a small negative `t` rounds
381
381
  // to into pass 0, so the pass is a plain integer whoever reads it.
382
- return { pass: Math.round((t - into) / period) + 0, into };
382
+ const pass = Math.round((t - into) / period) + 0;
383
+ // [LAW:no-silent-failure] Past where a pass is a float, `period` is finer than time resolves: the refusal a
384
+ // boundary walked there gives, said of `t` and not of the infinite sum a pass that overflowed would build.
385
+ if (!Number.isFinite(pass))
386
+ throw new RangeError(`${what}: a period of ${period} s is finer than time resolves near t = ${t}`);
387
+ return { pass, into };
383
388
  }
384
389
  /** The least normal double, 2^-1022: below it every double is a multiple of `Number.MIN_VALUE`. */
385
390
  const MIN_NORMAL = 2 ** -1022;
@@ -14,3 +14,9 @@
14
14
  * `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
15
15
  */
16
16
  export type Env = Readonly<Record<string, string | undefined>>;
17
+ /**
18
+ * A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
19
+ * as Emacs' shell mode sets it. It takes no colour and nothing that moves the
20
+ * cursor.
21
+ */
22
+ export declare function isDumbTerminal(env: Env): boolean;
package/dist/core/env.js CHANGED
@@ -13,4 +13,12 @@
13
13
  * without `@types/node` — the ordinary browser project — cannot resolve the
14
14
  * `NodeJS` namespace. `test/seam/browser-types.test.ts` holds that line.
15
15
  */
16
- export {};
16
+ /**
17
+ * A terminal that says it cannot be drawn on: `TERM` is `dumb` or `unknown`,
18
+ * as Emacs' shell mode sets it. It takes no colour and nothing that moves the
19
+ * cursor.
20
+ */
21
+ export function isDumbTerminal(env) {
22
+ const term = env["TERM"];
23
+ return term === "dumb" || term === "unknown";
24
+ }
@@ -121,13 +121,14 @@ function onGrid(text, draw) {
121
121
  * blinks with the glyph. A wide glyph's box sits between the two: it inherits
122
122
  * the double underline, and the inner span inside it draws the other lines.
123
123
  */
124
- function runHtml({ text, look }) {
124
+ function runHtml({ text, look, inVisualOrder }) {
125
125
  const drawn = look.underline === "double"
126
126
  ? span([...paintCss(look), "text-decoration-line:underline", "text-decoration-style:double", ...BLINK[look.blink]], onGrid(text, (glyph) => span([`color:${look.foreground.hex}`, ...glyphCss(look)], glyph)))
127
127
  : span([...paintCss(look), ...glyphCss(look)], onGrid(text, (glyph) => glyph));
128
+ const ordered = inVisualOrder ? span(["unicode-bidi:bidi-override"], drawn) : drawn;
128
129
  return look.href === null
129
- ? drawn
130
- : `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${drawn}</a>`;
130
+ ? ordered
131
+ : `<a href="${escapeAttribute(look.href)}" style="${ANCHOR_CSS}">${ordered}</a>`;
131
132
  }
132
133
  /**
133
134
  * The CSS a page includes once, wherever fragments appear: what an inline
@@ -65,6 +65,8 @@ export interface ExportLook {
65
65
  export interface ExportRun {
66
66
  readonly text: string;
67
67
  readonly look: ExportLook;
68
+ /** Its cells are in display order already, so a browser must not reorder them. */
69
+ readonly inVisualOrder: boolean;
68
70
  }
69
71
  /** One terminal row, left to right. An empty array is a blank row. */
70
72
  export type ExportLine = readonly ExportRun[];
@@ -31,6 +31,7 @@
31
31
  */
32
32
  import { blendRgb, ColorSpec } from "./color.js";
33
33
  import { NULL_STYLE } from "./style.js";
34
+ import { sealed } from "./seal.js";
34
35
  /**
35
36
  * The URL schemes an export will link.
36
37
  *
@@ -146,13 +147,16 @@ export function resolveLook(style, theme) {
146
147
  */
147
148
  export function exportLines(segments, theme) {
148
149
  const rows = [[]];
149
- for (const segment of segments) {
150
- const look = resolveLook(segment.style ?? NULL_STYLE, theme);
150
+ // A browser is a display that reorders and holds the controls at zero width.
151
+ for (const segment of sealed(segments, "logical")) {
152
+ const style = segment.style ?? NULL_STYLE;
153
+ const look = resolveLook(style, theme);
154
+ const inVisualOrder = style.bidi?.order === "visual";
151
155
  segment.text.split("\n").forEach((piece, index) => {
152
156
  if (index > 0)
153
157
  rows.push([]);
154
158
  if (piece.length > 0)
155
- rows[rows.length - 1].push({ text: piece, look });
159
+ rows[rows.length - 1].push({ text: piece, look, inVisualOrder });
156
160
  });
157
161
  }
158
162
  return rows[rows.length - 1].length === 0 ? rows.slice(0, -1) : rows;