@promptctl/rich-js 0.27.0 → 0.29.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 (185) hide show
  1. package/README.md +7 -6
  2. package/THIRD-PARTY-NOTICES +42 -0
  3. package/dist/core/animation.d.ts +65 -46
  4. package/dist/core/animation.js +91 -67
  5. package/dist/core/ansi.d.ts +19 -12
  6. package/dist/core/ansi.js +114 -36
  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 +94 -6
  17. package/dist/core/clock.d.ts +1 -1
  18. package/dist/core/color.d.ts +56 -57
  19. package/dist/core/color.js +79 -81
  20. package/dist/core/console.d.ts +24 -7
  21. package/dist/core/console.js +27 -8
  22. package/dist/core/control-codes.d.ts +61 -0
  23. package/dist/core/control-codes.js +157 -0
  24. package/dist/core/easing.d.ts +40 -40
  25. package/dist/core/easing.js +40 -40
  26. package/dist/core/env.d.ts +6 -0
  27. package/dist/core/env.js +9 -1
  28. package/dist/core/export-html.js +5 -4
  29. package/dist/core/export-lines.d.ts +13 -11
  30. package/dist/core/export-lines.js +19 -15
  31. package/dist/core/export-svg.d.ts +3 -3
  32. package/dist/core/export-svg.js +10 -9
  33. package/dist/core/json.js +9 -2
  34. package/dist/core/markup.js +5 -4
  35. package/dist/core/memo.d.ts +1 -1
  36. package/dist/core/memo.js +1 -1
  37. package/dist/core/oklch.d.ts +14 -14
  38. package/dist/core/oklch.js +10 -10
  39. package/dist/core/orient.d.ts +68 -0
  40. package/dist/core/orient.js +224 -0
  41. package/dist/core/osc8.d.ts +12 -5
  42. package/dist/core/osc8.js +14 -7
  43. package/dist/core/paint.d.ts +9 -9
  44. package/dist/core/paint.js +3 -3
  45. package/dist/core/pretty.d.ts +1 -1
  46. package/dist/core/pretty.js +23 -19
  47. package/dist/core/protocol.d.ts +47 -26
  48. package/dist/core/protocol.js +31 -23
  49. package/dist/core/render.d.ts +19 -14
  50. package/dist/core/render.js +30 -17
  51. package/dist/core/seal.d.ts +26 -0
  52. package/dist/core/seal.js +75 -0
  53. package/dist/core/segment.d.ts +12 -4
  54. package/dist/core/segment.js +16 -6
  55. package/dist/core/strip.d.ts +83 -83
  56. package/dist/core/strip.js +70 -70
  57. package/dist/core/style.d.ts +46 -18
  58. package/dist/core/style.js +92 -56
  59. package/dist/core/text-order.d.ts +51 -0
  60. package/dist/core/text-order.js +50 -0
  61. package/dist/core/text.d.ts +12 -2
  62. package/dist/core/text.js +136 -90
  63. package/dist/core/timeline.d.ts +5 -5
  64. package/dist/core/timeline.js +11 -11
  65. package/dist/core/tween.d.ts +6 -6
  66. package/dist/core/tween.js +10 -10
  67. package/dist/core/view.d.ts +5 -5
  68. package/dist/core/view.js +15 -14
  69. package/dist/core/wrap.d.ts +24 -6
  70. package/dist/core/wrap.js +82 -16
  71. package/dist/host/app.d.ts +50 -9
  72. package/dist/host/app.js +120 -15
  73. package/dist/host/host-environment.d.ts +2 -2
  74. package/dist/host/host-environment.js +2 -2
  75. package/dist/host/index.d.ts +3 -2
  76. package/dist/host/index.js +1 -0
  77. package/dist/host/input.d.ts +42 -9
  78. package/dist/host/input.js +91 -7
  79. package/dist/host/terminal-colors.d.ts +18 -0
  80. package/dist/host/terminal-colors.js +46 -0
  81. package/dist/host/terminal-host.d.ts +4 -3
  82. package/dist/host/terminal-host.js +1 -1
  83. package/dist/index.d.ts +17 -16
  84. package/dist/index.js +11 -11
  85. package/dist/node/prompt.d.ts +1 -1
  86. package/dist/node/prompt.js +1 -1
  87. package/dist/node/terminal-host.d.ts +1 -1
  88. package/dist/node/traceback.js +2 -2
  89. package/dist/renderables/change.d.ts +1 -3
  90. package/dist/renderables/change.js +6 -8
  91. package/dist/renderables/effect.d.ts +88 -87
  92. package/dist/renderables/effect.js +124 -114
  93. package/dist/renderables/effects.d.ts +105 -76
  94. package/dist/renderables/effects.js +309 -176
  95. package/dist/renderables/embed.d.ts +1 -1
  96. package/dist/renderables/embed.js +2 -2
  97. package/dist/renderables/flexStrip.d.ts +7 -7
  98. package/dist/renderables/flexStrip.js +15 -15
  99. package/dist/renderables/frames.d.ts +7 -16
  100. package/dist/renderables/frames.js +12 -27
  101. package/dist/renderables/glyph-effect.d.ts +22 -23
  102. package/dist/renderables/glyph-effect.js +43 -41
  103. package/dist/renderables/glyph-effects.d.ts +33 -14
  104. package/dist/renderables/glyph-effects.js +90 -22
  105. package/dist/renderables/html-entities.d.ts +1 -1
  106. package/dist/renderables/html-entities.js +1 -1
  107. package/dist/renderables/lifecycle.d.ts +8 -10
  108. package/dist/renderables/lifecycle.js +17 -18
  109. package/dist/renderables/live.d.ts +2 -2
  110. package/dist/renderables/live.js +7 -7
  111. package/dist/renderables/markdown-parse.js +3 -3
  112. package/dist/renderables/markdown.js +4 -2
  113. package/dist/renderables/panel.js +7 -5
  114. package/dist/renderables/progress.d.ts +1 -1
  115. package/dist/renderables/progress.js +1 -1
  116. package/dist/renderables/progressBar.d.ts +3 -3
  117. package/dist/renderables/progressBar.js +21 -18
  118. package/dist/renderables/prompt.d.ts +1 -1
  119. package/dist/renderables/scroll.d.ts +1 -1
  120. package/dist/renderables/scroll.js +3 -3
  121. package/dist/renderables/spinner.d.ts +4 -4
  122. package/dist/renderables/spinner.js +10 -10
  123. package/dist/renderables/syntax.d.ts +2 -2
  124. package/dist/renderables/syntax.js +9 -4
  125. package/dist/renderables/table.js +5 -4
  126. package/dist/renderables/traceback.d.ts +1 -1
  127. package/dist/renderables/traceback.js +1 -1
  128. package/dist/renderables/tree.js +3 -3
  129. package/dist/template-bindings/animation-funcs.d.ts +60 -29
  130. package/dist/template-bindings/animation-funcs.js +126 -43
  131. package/dist/template-bindings/color-funcs.d.ts +14 -14
  132. package/dist/template-bindings/color-funcs.js +25 -25
  133. package/dist/template-bindings/index.d.ts +16 -14
  134. package/dist/template-bindings/index.js +17 -14
  135. package/dist/template-bindings/palette-funcs.d.ts +2 -2
  136. package/dist/template-bindings/palette-funcs.js +4 -4
  137. package/dist/template-bindings/style-funcs.d.ts +5 -5
  138. package/dist/template-bindings/style-funcs.js +16 -16
  139. package/dist/themes/{transpose.d.ts → adjust.d.ts} +23 -23
  140. package/dist/themes/{transpose.js → adjust.js} +28 -28
  141. package/dist/themes/buildPalette.d.ts +6 -5
  142. package/dist/themes/buildPalette.js +22 -22
  143. package/dist/themes/colorMath.d.ts +16 -16
  144. package/dist/themes/colorMath.js +32 -32
  145. package/dist/themes/data/ansi-tables.d.ts +2 -2
  146. package/dist/themes/data/ansi-tables.js +2 -2
  147. package/dist/themes/data/index.js +1 -4
  148. package/dist/themes/data/textual-ansi.js +0 -143
  149. package/dist/themes/data/types.d.ts +4 -4
  150. package/dist/themes/data/types.js +2 -2
  151. package/dist/themes/palette.d.ts +12 -12
  152. package/dist/themes/palette.js +16 -16
  153. package/dist/themes/ramp.d.ts +17 -17
  154. package/dist/themes/ramp.js +12 -12
  155. package/dist/themes/registry.d.ts +6 -8
  156. package/dist/themes/registry.js +6 -8
  157. package/dist/themes/terminalThemes.d.ts +4 -11
  158. package/dist/themes/terminalThemes.js +31 -47
  159. package/dist/widgets/button.d.ts +3 -3
  160. package/dist/widgets/button.js +8 -8
  161. package/dist/widgets/checkbox.d.ts +2 -2
  162. package/dist/widgets/checkbox.js +4 -4
  163. package/dist/widgets/dropdown.d.ts +3 -3
  164. package/dist/widgets/dropdown.js +7 -5
  165. package/dist/widgets/event-router.d.ts +6 -6
  166. package/dist/widgets/event-router.js +2 -2
  167. package/dist/widgets/focus-manager.d.ts +3 -3
  168. package/dist/widgets/focus-manager.js +3 -3
  169. package/dist/widgets/index.d.ts +2 -2
  170. package/dist/widgets/index.js +1 -1
  171. package/dist/widgets/ink.d.ts +3 -3
  172. package/dist/widgets/ink.js +4 -4
  173. package/dist/widgets/slider.d.ts +2 -2
  174. package/dist/widgets/static-item.d.ts +2 -2
  175. package/dist/widgets/text-input.d.ts +2 -2
  176. package/dist/widgets/text-input.js +5 -5
  177. package/dist/widgets/themed-widget.js +4 -4
  178. package/dist/widgets/toggle.d.ts +4 -4
  179. package/dist/widgets/toggle.js +6 -6
  180. package/dist/widgets/types.d.ts +6 -6
  181. package/dist/widgets/types.js +2 -2
  182. package/dist/widgets/widget-app.d.ts +10 -10
  183. package/dist/widgets/widget-app.js +8 -8
  184. package/dist/widgets/widget-base.d.ts +2 -2
  185. package/package.json +5 -4
@@ -22,9 +22,9 @@
22
22
  * anything it draws. What the drawn frame says about when it changes is on
23
23
  * the timeline's clock, and `drawView` carries it back onto the host's by
24
24
  * running the frame through the same timeline: the frame is a sample held
25
- * for the intersection of its cells' holds (`Segment.lastChange`,
26
- * `Segment.nextChange`) and the holds it was told that no cell carries
27
- * (`RenderOptions.onHold`), and `Timeline.run` is the one place a hold is
25
+ * for the intersection of its cells' holds (`Segment.validFrom`,
26
+ * `Segment.validUntil`) and the holds it was told that no cell carries
27
+ * (`RenderOptions.onValidity`), and `Timeline.run` is the one place a hold is
28
28
  * mapped onto host time, forwards, backwards or paused.
29
29
  *
30
30
  * What a host does with that is its own policy, and it reads it off the
@@ -33,7 +33,7 @@
33
33
  * auto-refresh, polls every frame.
34
34
  *
35
35
  * Tier 7 of `src/core/`: `./animation.ts`'s `Animation`, `./timeline.ts`,
36
- * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
36
+ * `./segment.ts`, `./protocol.ts` and `./style.ts`'s holds, nothing else.
37
37
  * [LAW:one-way-deps]
38
38
  */
39
39
  import { type HostFrame, type Renderable, type RenderOptions } from "./protocol.js";
@@ -86,7 +86,7 @@ export interface FrameSays {
86
86
  /** The host time the frame was drawn at. */
87
87
  readonly t: number;
88
88
  /** The first host moment, never before `t`, at which the frame may stop showing this: `t` while it moves, `Infinity` once it never will. */
89
- readonly nextChange: number;
89
+ readonly validUntil: number;
90
90
  }
91
91
  /** A frame drawn from a view: its lines, and what it says. */
92
92
  export interface DrawnView extends FrameSays {
package/dist/core/view.js CHANGED
@@ -22,9 +22,9 @@
22
22
  * anything it draws. What the drawn frame says about when it changes is on
23
23
  * the timeline's clock, and `drawView` carries it back onto the host's by
24
24
  * running the frame through the same timeline: the frame is a sample held
25
- * for the intersection of its cells' holds (`Segment.lastChange`,
26
- * `Segment.nextChange`) and the holds it was told that no cell carries
27
- * (`RenderOptions.onHold`), and `Timeline.run` is the one place a hold is
25
+ * for the intersection of its cells' holds (`Segment.validFrom`,
26
+ * `Segment.validUntil`) and the holds it was told that no cell carries
27
+ * (`RenderOptions.onValidity`), and `Timeline.run` is the one place a hold is
28
28
  * mapped onto host time, forwards, backwards or paused.
29
29
  *
30
30
  * What a host does with that is its own policy, and it reads it off the
@@ -33,11 +33,12 @@
33
33
  * auto-refresh, polls every frame.
34
34
  *
35
35
  * Tier 7 of `src/core/`: `./animation.ts`'s `Animation`, `./timeline.ts`,
36
- * `./segment.ts`, `./protocol.ts` and `./style.ts`'s `Hold`, nothing else.
36
+ * `./segment.ts`, `./protocol.ts` and `./style.ts`'s holds, nothing else.
37
37
  * [LAW:one-way-deps]
38
38
  */
39
39
  import { activePass, render } from "./protocol.js";
40
40
  import { Segment } from "./segment.js";
41
+ import { intersection, NEVER_CHANGES } from "./style.js";
41
42
  import { Timeline } from "./timeline.js";
42
43
  /** `view` as the renderable a host holds: a function is asked each time it is drawn. */
43
44
  export function hostView(view) {
@@ -77,9 +78,9 @@ export function frameNow(host, time) {
77
78
  return { time: frameTime(time), host };
78
79
  }
79
80
  /** What a frame not yet drawn at host time `t` says: nothing. */
80
- export const undrawn = (t) => ({ t, nextChange: Number.POSITIVE_INFINITY });
81
+ export const undrawn = (t) => ({ t, validUntil: Number.POSITIVE_INFINITY });
81
82
  /** The hold of the frame `lines` draw, told `told` besides: the intersection of its cells' holds and it. */
82
- const frameHold = (lines, told) => lines.reduce((hold, row) => ({ lastChange: Math.max(hold.lastChange, Segment.lastChange(row)), nextChange: Math.min(hold.nextChange, Segment.nextChange(row)) }), told);
83
+ const frameValidity = (lines, told) => lines.reduce((validity, row) => intersection(validity, { validFrom: Segment.validFrom(row), validUntil: Segment.validUntil(row) }), told);
83
84
  /**
84
85
  * `view` drawn by `drawnBy` at `time` under `options`, its lines shaped by
85
86
  * `draw` — the host's own render under those options, with its height and
@@ -91,17 +92,17 @@ const frameHold = (lines, told) => lines.reduce((hold, row) => ({ lastChange: Ma
91
92
  */
92
93
  export function drawView(view, time, drawnBy, options, draw) {
93
94
  const { now: host, timeline } = time;
94
- let told = { lastChange: Number.NEGATIVE_INFINITY, nextChange: Number.POSITIVE_INFINITY };
95
- const onHold = (hold) => {
96
- told = { lastChange: Math.max(told.lastChange, hold.lastChange), nextChange: Math.min(told.nextChange, hold.nextChange) };
95
+ let told = NEVER_CHANGES;
96
+ const onValidity = (validity) => {
97
+ told = intersection(told, validity);
97
98
  };
98
99
  // Made before the render opens: the one time the frame has.
99
100
  const frame = frameNow(drawnBy, time);
100
- const lines = draw(view, { ...options, frame, onHold });
101
- const hold = frameHold(lines, told);
101
+ const lines = draw(view, { ...options, frame, onValidity });
102
+ const validity = frameValidity(lines, told);
102
103
  // The frame as the sample of the view it is, on the timeline's clock; the
103
104
  // timeline says when the host's clock leaves its hold.
104
- const sample = { at: () => lines, nextChange: () => hold.nextChange, lastChange: () => hold.lastChange, reduced: () => sample };
105
- const nextChange = Timeline.run(timeline, sample).nextChange(host);
106
- return { t: host, lines, nextChange };
105
+ const sample = { at: () => lines, validUntil: () => validity.validUntil, validFrom: () => validity.validFrom, reduced: () => sample };
106
+ const validUntil = Timeline.run(timeline, sample).validUntil(host);
107
+ return { t: host, lines, validUntil };
107
108
  }
@@ -18,16 +18,30 @@
18
18
  * line still too wide once wrapping is done. That order is the whole reason a
19
19
  * long sentence in a table cell grows the row while an unbreakable word in the
20
20
  * same cell still ellipsizes.
21
+ *
22
+ * Rich breaks only at whitespace, and that is what happens to any word holding
23
+ * nothing below: `test/core/text-wrap.golden.txt` pins it. Past that, a word
24
+ * breaks where its text says it may (UAX #14): after ZWSP, at a soft hyphen, and
25
+ * between the words of a script written without spaces. U+2060 and U+FEFF forbid
26
+ * a break, which here needs nothing but not being one.
21
27
  */
22
- import { type CellCol } from "./cells.js";
28
+ import { type CellCol, type CodePoint } from "./cells.js";
29
+ /** One place a line of text was cut. */
30
+ export interface LineBreak {
31
+ /** Where the line is cut, in cells. */
32
+ readonly cell: CellCol;
33
+ /** The soft hyphen this break draws as `-`. Absent at any other break, and where no hyphen fits. */
34
+ readonly hyphen: CodePoint | undefined;
35
+ }
23
36
  /**
24
- * The cells each word of `text` needs, in order — the widest is the narrowest
25
- * width `divideLine` can wrap it to without cutting a word.
37
+ * The cells each piece of `text` needs, in order — the widest is the narrowest
38
+ * width `divideLine` can wrap it to without cutting a word. A hyphen is not in
39
+ * them: where it does not fit, the break goes without it.
26
40
  */
27
41
  export declare function wordWidths(text: string): number[];
28
42
  /**
29
- * The cell offsets `text` should be cut at to wrap it into lines of at most
30
- * `width` cells, for `Segment.divide` to apply to the styled line.
43
+ * The breaks that wrap `text` into lines of at most `width` cells, for
44
+ * `Segment.divide` to apply to the styled line.
31
45
  *
32
46
  * `fold` decides what happens to a word that fits on no line at all: folded,
33
47
  * it is chopped across as many lines as it needs; unfolded, it is left whole
@@ -36,6 +50,10 @@ export declare function wordWidths(text: string): number[];
36
50
  * can truncate it, which is what makes an unbreakable word ellipsize while the
37
51
  * sentence around it merely wraps.
38
52
  *
53
+ * A piece that ends in a soft hyphen stays on a line only with room for the
54
+ * hyphen, so a line it closes holds it, and a word that fits whole needs none. A
55
+ * piece as wide as the line breaks with no hyphen rather than lose a letter to one.
56
+ *
39
57
  * Requires `width >= 1`. A zero-cell canvas gets no cuts, because "no cut
40
58
  * fits" and "no cell fits" are different facts and only one of them is a list
41
59
  * of offsets — the caller answers the second one by cropping the single
@@ -43,4 +61,4 @@ export declare function wordWidths(text: string): number[];
43
61
  */
44
62
  export declare function divideLine(text: string, width: CellCol, options: {
45
63
  fold: boolean;
46
- }): CellCol[];
64
+ }): LineBreak[];
package/dist/core/wrap.js CHANGED
@@ -18,8 +18,28 @@
18
18
  * line still too wide once wrapping is done. That order is the whole reason a
19
19
  * long sentence in a table cell grows the row while an unbreakable word in the
20
20
  * same cell still ellipsizes.
21
+ *
22
+ * Rich breaks only at whitespace, and that is what happens to any word holding
23
+ * nothing below: `test/core/text-wrap.golden.txt` pins it. Past that, a word
24
+ * breaks where its text says it may (UAX #14): after ZWSP, at a soft hyphen, and
25
+ * between the words of a script written without spaces. U+2060 and U+FEFF forbid
26
+ * a break, which here needs nothing but not being one.
21
27
  */
22
- import { cellLen, chopCells, asCellCol } from "./cells.js";
28
+ import { asCellCol, asCodePoint, cellLen, chopCells } from "./cells.js";
29
+ import { pattern } from "./pattern.js";
30
+ const SOFT_HYPHEN = "\u00ad";
31
+ const ZWSP = "\u200b";
32
+ // The scripts UAX #14 sends to a dictionary (line break class SA) and ICU has one
33
+ // for. Which words it finds varies by ICU, and a dictionary cannot be vendored as
34
+ // the width table is; a word it misses is folded like any other.
35
+ const UNSPACED = "[\\p{Script_Extensions=Thai}\\p{Script_Extensions=Lao}\\p{Script_Extensions=Khmer}\\p{Script_Extensions=Myanmar}]+";
36
+ const BREAKABLE = pattern(new RegExp(`[${SOFT_HYPHEN}${ZWSP}]|${UNSPACED}`, "u"));
37
+ const WORD_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "word" });
38
+ // A break needs a glyph on both sides of it: with none before, nothing breaks away
39
+ // or takes a hyphen, and with none after, the word was already over.
40
+ const UNDRAWN = "\\s\\p{Cf}\\p{Mn}";
41
+ const OPENING = pattern(new RegExp(`^[${UNDRAWN}]*`, "u"));
42
+ const THROUGH_LAST_GLYPH = pattern(new RegExp(`^[^]*[^${UNDRAWN}]`, "u"));
23
43
  /**
24
44
  * Each word of `text` in order, where a "word" carries its own trailing
25
45
  * whitespace — the run of spaces that follows it belongs to it, because that
@@ -34,7 +54,9 @@ function* words(text) {
34
54
  // Constructed per call rather than hoisted: a sticky regex carries its
35
55
  // `lastIndex` as mutable state, and a shared one would corrupt any two
36
56
  // interleaved walks. [LAW:no-shared-mutable-globals]
37
- const re = /\s*\S+\s*/y;
57
+ // U+FEFF is outside it, as it is outside Python's `\s` and Rich's words: it is a
58
+ // no-break space that JavaScript's `\s` counts as a space.
59
+ const re = /[^\S\ufeff]*[\S\ufeff]+[^\S\ufeff]*/y;
38
60
  for (let match = re.exec(text); match !== null; match = re.exec(text)) {
39
61
  yield match[0];
40
62
  }
@@ -47,12 +69,46 @@ function* words(text) {
47
69
  function widthOf(word) {
48
70
  return cellLen(word.trimEnd());
49
71
  }
72
+ /** The offsets inside `word`, ascending, that it may break at. */
73
+ function breaksIn(word) {
74
+ if (word.match(BREAKABLE) === null)
75
+ return [];
76
+ const found = new Set();
77
+ // One break to a run of marks: a second would open a piece with nothing in it.
78
+ for (const { 0: mark, index } of word.matchAll(new RegExp(`(?<![${SOFT_HYPHEN}${ZWSP}])[${SOFT_HYPHEN}${ZWSP}]`, "gu")))
79
+ found.add(index + mark.length);
80
+ for (const { 0: run, index } of word.matchAll(new RegExp(UNSPACED, "gu"))) {
81
+ // A run's start is where the segmenter's input began, not a boundary it found:
82
+ // UAX #14 has these scripts break as letters do against what comes before them.
83
+ for (const { index: at } of WORD_SEGMENTER.segment(run))
84
+ if (at > 0)
85
+ found.add(index + at);
86
+ }
87
+ const start = OPENING.exec(word)[0].length;
88
+ const end = THROUGH_LAST_GLYPH.exec(word)?.[0].length ?? 0;
89
+ return [...found].filter((at) => at > start && at < end).sort((a, b) => a - b);
90
+ }
91
+ /** `text` as what wraps: its words, tiled further wherever one may break. */
92
+ function* piecesOf(text) {
93
+ for (const word of words(text)) {
94
+ let start = 0;
95
+ for (const at of breaksIn(word)) {
96
+ const piece = word.slice(start, at);
97
+ const cells = widthOf(piece);
98
+ yield { text: piece, cells, hyphenates: cells > 0 && piece.endsWith(SOFT_HYPHEN) };
99
+ start = at;
100
+ }
101
+ const last = word.slice(start);
102
+ yield { text: last, cells: widthOf(last), hyphenates: false };
103
+ }
104
+ }
50
105
  /**
51
- * The cells each word of `text` needs, in order — the widest is the narrowest
52
- * width `divideLine` can wrap it to without cutting a word.
106
+ * The cells each piece of `text` needs, in order — the widest is the narrowest
107
+ * width `divideLine` can wrap it to without cutting a word. A hyphen is not in
108
+ * them: where it does not fit, the break goes without it.
53
109
  */
54
110
  export function wordWidths(text) {
55
- return Array.from(words(text), widthOf);
111
+ return Array.from(piecesOf(text), (piece) => piece.cells);
56
112
  }
57
113
  /**
58
114
  * A word chopped into pieces of at most `width` cells, as the reference chops
@@ -78,8 +134,8 @@ function foldWord(word, width) {
78
134
  return cellLen(pieces[0]) > width ? ["", ...pieces] : pieces;
79
135
  }
80
136
  /**
81
- * The cell offsets `text` should be cut at to wrap it into lines of at most
82
- * `width` cells, for `Segment.divide` to apply to the styled line.
137
+ * The breaks that wrap `text` into lines of at most `width` cells, for
138
+ * `Segment.divide` to apply to the styled line.
83
139
  *
84
140
  * `fold` decides what happens to a word that fits on no line at all: folded,
85
141
  * it is chopped across as many lines as it needs; unfolded, it is left whole
@@ -88,6 +144,10 @@ function foldWord(word, width) {
88
144
  * can truncate it, which is what makes an unbreakable word ellipsize while the
89
145
  * sentence around it merely wraps.
90
146
  *
147
+ * A piece that ends in a soft hyphen stays on a line only with room for the
148
+ * hyphen, so a line it closes holds it, and a word that fits whole needs none. A
149
+ * piece as wide as the line breaks with no hyphen rather than lose a letter to one.
150
+ *
91
151
  * Requires `width >= 1`. A zero-cell canvas gets no cuts, because "no cut
92
152
  * fits" and "no cell fits" are different facts and only one of them is a list
93
153
  * of offsets — the caller answers the second one by cropping the single
@@ -96,19 +156,22 @@ function foldWord(word, width) {
96
156
  export function divideLine(text, width, options) {
97
157
  if (width <= 0)
98
158
  return [];
99
- const cuts = [];
159
+ const breaks = [];
100
160
  // A cut at the start of the text is not a cut: it would open the line with
101
161
  // an empty piece. [LAW:single-enforcer] for that rule, so neither arm below
102
162
  // has to restate it.
103
163
  const cutAt = (cell) => {
104
164
  if (cell > 0)
105
- cuts.push(asCellCol(cell));
165
+ breaks.push({ cell: asCellCol(cell), hyphen: width - used > 0 ? softHyphen : undefined });
106
166
  };
107
167
  let used = 0; // cells already committed to the line being filled
108
168
  let wordStart = 0; // absolute cell offset where the current word begins
109
- for (const word of words(text)) {
110
- const wordWidth = widthOf(word);
111
- if (width - used >= wordWidth) {
169
+ let offset = 0; // code units before the current word
170
+ let softHyphen; // the one the line being filled ends in
171
+ for (const piece of piecesOf(text)) {
172
+ const word = piece.text;
173
+ const wordWidth = piece.cells;
174
+ if (width - used >= wordWidth + (piece.hyphenates ? 1 : 0)) {
112
175
  used += cellLen(word);
113
176
  }
114
177
  else if (options.fold && wordWidth > width) {
@@ -119,13 +182,14 @@ export function divideLine(text, width, options) {
119
182
  let pieceStart = wordStart;
120
183
  for (let index = 0; index < pieces.length; index += 1) {
121
184
  cutAt(pieceStart);
122
- const piece = pieces[index];
185
+ softHyphen = undefined;
186
+ const fold = pieces[index];
123
187
  // The last piece is what the *next* word finds already on the line;
124
188
  // the rest are whole lines of their own.
125
189
  if (index === pieces.length - 1)
126
- used = cellLen(piece);
190
+ used = cellLen(fold);
127
191
  else
128
- pieceStart += cellLen(piece);
192
+ pieceStart += cellLen(fold);
129
193
  }
130
194
  }
131
195
  else {
@@ -135,6 +199,8 @@ export function divideLine(text, width, options) {
135
199
  used = cellLen(word);
136
200
  }
137
201
  wordStart += cellLen(word);
202
+ offset += word.length;
203
+ softHyphen = piece.hyphenates ? asCodePoint(offset - 1) : undefined;
138
204
  }
139
- return cuts;
205
+ return breaks;
140
206
  }
@@ -31,6 +31,7 @@
31
31
  * — a focused field in a `WidgetApp` included — can swallow them.
32
32
  */
33
33
  import { type Clock, type FrameRate } from "../core/clock.js";
34
+ import { type TerminalColors } from "../core/color.js";
34
35
  import { type ConsoleOptions } from "../core/console.js";
35
36
  import { Segment } from "../core/segment.js";
36
37
  import { type Surface } from "../core/paint.js";
@@ -38,7 +39,7 @@ import { type HostFrame, type FrameHost } from "../core/protocol.js";
38
39
  import type { Unsubscribe } from "../core/subscription.js";
39
40
  import { Timeline } from "../core/timeline.js";
40
41
  import { type View } from "../core/view.js";
41
- import { type KeyPress, type ScreenMouseEvent } from "./input.js";
42
+ import { type KeyEvent, type TerminalMouseEvent, type TerminalReply } from "./input.js";
42
43
  import type { TerminalHost } from "./terminal-host.js";
43
44
  /**
44
45
  * Where an app is in its life. `idle` has not run; `running` holds the
@@ -46,7 +47,7 @@ import type { TerminalHost } from "./terminal-host.js";
46
47
  * the program; `stopped` has handed it back for good.
47
48
  */
48
49
  export type AppPhase = "idle" | "running" | "suspended" | "stopped";
49
- declare const DRAW_OPTIONS: readonly ["asciiOnly", "reducedMotion", "theme", "onStyleError", "markup", "highlight", "highlighter"];
50
+ declare const DRAW_OPTIONS: readonly ["asciiOnly", "reducedMotion", "textOrder", "terminal", "theme", "onStyleError", "markup", "highlight", "highlighter"];
50
51
  type DrawOption = (typeof DRAW_OPTIONS)[number];
51
52
  type DrawOptions = Readonly<Pick<ConsoleOptions, DrawOption>>;
52
53
  export interface AppOptions extends DrawOptions {
@@ -88,7 +89,7 @@ export interface AppOptions extends DrawOptions {
88
89
  * A turn of the wheel, where the pointer was when it turned: `lines` is
89
90
  * positive towards the end of the content, a line a notch.
90
91
  */
91
- export interface Wheel {
92
+ export interface WheelEvent {
92
93
  readonly x: number;
93
94
  readonly y: number;
94
95
  readonly lines: number;
@@ -97,11 +98,26 @@ declare const WHEEL_LINES: {
97
98
  readonly scroll_up: -1;
98
99
  readonly scroll_down: 1;
99
100
  };
100
- type WheelTurn = Extract<ScreenMouseEvent, {
101
+ type WheelTurn = Extract<TerminalMouseEvent, {
101
102
  type: keyof typeof WHEEL_LINES;
102
103
  }>;
103
104
  /** A pointer event `onMouse` hears: every one but the vertical wheel, which is `onWheel`'s. */
104
- export type AppMouseEvent = Exclude<ScreenMouseEvent, WheelTurn>;
105
+ export type AppMouseEvent = Exclude<TerminalMouseEvent, WheelTurn>;
106
+ /**
107
+ * How an ask ended, with the replies heard before it did. `answered`: the
108
+ * terminal answered DA1, so a query with no reply among `replies` is one it
109
+ * does not support. `timeout`: it answered nothing in time, DA1 included.
110
+ * `left`: the app handed the terminal back first. `unasked`: the host writes
111
+ * to no terminal, so nothing was asked.
112
+ */
113
+ export interface Asked {
114
+ readonly ended: "answered" | "timeout" | "left" | "unasked";
115
+ readonly replies: readonly TerminalReply[];
116
+ }
117
+ export interface AskOptions {
118
+ /** Seconds to wait for a terminal that answers nothing. Half a second, unless given. */
119
+ readonly timeout?: number;
120
+ }
105
121
  export declare class App implements FrameHost {
106
122
  private readonly host;
107
123
  private readonly surface;
@@ -113,7 +129,11 @@ export declare class App implements FrameHost {
113
129
  private ticking;
114
130
  private said;
115
131
  private waking;
116
- private readonly console;
132
+ private console;
133
+ private drawing;
134
+ private asking;
135
+ private asked;
136
+ private owedDa1;
117
137
  private _phase;
118
138
  private _frame;
119
139
  private readonly painter;
@@ -127,6 +147,26 @@ export declare class App implements FrameHost {
127
147
  private readonly input;
128
148
  private settle;
129
149
  constructor(options: AppOptions);
150
+ /**
151
+ * The terminal the app draws on: the colors a cell that sets none is drawn
152
+ * in, and the sixteen an ANSI color names — as given, or as the color
153
+ * depth assumes (`resolveTerminal`).
154
+ */
155
+ get terminal(): TerminalColors;
156
+ /** Draw on `terminal` from the next frame on — what `terminalColors` learned, say. */
157
+ set terminal(terminal: TerminalColors);
158
+ /**
159
+ * Ask the terminal: write `queries`, then DA1, and resolve with the replies
160
+ * heard before DA1's, or before the timeout or the app handing the terminal
161
+ * back. It rejects only when writing to the terminal throws. Asks run one
162
+ * after another, and only while the app runs: it holds raw mode and reads
163
+ * the input, so a reply reaches the ask and never a key handler. A reply no
164
+ * ask is waiting for is dropped. `queries` must not ask for DA1 itself: its
165
+ * reply would end the ask early.
166
+ */
167
+ ask(queries: readonly string[], options?: AskOptions): Promise<Asked>;
168
+ private drawingConsole;
169
+ private askNow;
130
170
  get phase(): AppPhase;
131
171
  /** The clock the app reads the time from: where the host time a `Timeline` operation takes is read. */
132
172
  get clock(): Clock;
@@ -186,10 +226,10 @@ export declare class App implements FrameHost {
186
226
  */
187
227
  onPaint(handler: (frame: readonly (readonly Segment[])[]) => void): Unsubscribe;
188
228
  /**
189
- * Hear every key the app does not quit on, by the `KeyPress` the terminal
229
+ * Hear every key the app does not quit on, by the `KeyEvent` the terminal
190
230
  * reported; `keyName` names it. A handler that throws fails the app.
191
231
  */
192
- onKey(handler: (press: KeyPress) => void): Unsubscribe;
232
+ onKey(handler: (press: KeyEvent) => void): Unsubscribe;
193
233
  /**
194
234
  * Hear every pointer event but the vertical wheel, which is `onWheel`'s,
195
235
  * in terminal cells. Only the alternate surface reports the pointer. A
@@ -204,7 +244,7 @@ export declare class App implements FrameHost {
204
244
  * and the first that moves something takes it. A handler that throws fails
205
245
  * the app.
206
246
  */
207
- onWheel(handler: (wheel: Wheel) => boolean): Unsubscribe;
247
+ onWheel(handler: (wheel: WheelEvent) => boolean): Unsubscribe;
208
248
  /** Hand the terminal back for good; `run` resolves. */
209
249
  stop(): void;
210
250
  /**
@@ -217,6 +257,7 @@ export declare class App implements FrameHost {
217
257
  private enter;
218
258
  private leave;
219
259
  private end;
260
+ private reply;
220
261
  private key;
221
262
  private mouse;
222
263
  private hear;