@promptctl/rich-js 0.18.1 → 0.20.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.
- package/README.md +11 -8
- package/dist/core/box.d.ts +3 -1
- package/dist/core/box.js +6 -13
- package/dist/core/cells.d.ts +61 -28
- package/dist/core/cells.js +171 -124
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +99 -18
- package/dist/core/color.js +334 -146
- package/dist/core/console.d.ts +38 -7
- package/dist/core/console.js +159 -90
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.js +1 -4
- package/dist/core/export-lines.d.ts +22 -13
- package/dist/core/export-lines.js +37 -19
- package/dist/core/export-svg.d.ts +49 -0
- package/dist/core/export-svg.js +213 -0
- package/dist/core/fnv1a.d.ts +10 -0
- package/dist/core/fnv1a.js +17 -0
- package/dist/core/highlighter.d.ts +2 -2
- package/dist/core/highlighter.js +113 -47
- package/dist/core/json.js +1 -1
- package/dist/core/markup.d.ts +28 -7
- package/dist/core/markup.js +235 -312
- package/dist/core/measure.d.ts +3 -3
- package/dist/core/measure.js +12 -7
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/noise.d.ts +31 -0
- package/dist/core/noise.js +94 -0
- package/dist/core/osc8.d.ts +6 -6
- package/dist/core/osc8.js +9 -21
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +161 -0
- package/dist/core/place.d.ts +1 -1
- package/dist/core/place.js +4 -6
- package/dist/core/pretty.d.ts +126 -2
- package/dist/core/pretty.js +571 -105
- package/dist/core/protocol.d.ts +43 -3
- package/dist/core/protocol.js +23 -2
- package/dist/core/render.d.ts +33 -30
- package/dist/core/render.js +43 -83
- package/dist/core/segment.d.ts +6 -5
- package/dist/core/segment.js +11 -15
- package/dist/core/selection.d.ts +12 -0
- package/dist/core/selection.js +14 -0
- package/dist/core/strip.d.ts +116 -74
- package/dist/core/strip.js +266 -217
- package/dist/core/style.d.ts +27 -7
- package/dist/core/style.js +106 -41
- package/dist/core/text.d.ts +43 -16
- package/dist/core/text.js +180 -59
- package/dist/core/wrap.d.ts +5 -0
- package/dist/core/wrap.js +16 -3
- package/dist/host/app.d.ts +95 -17
- package/dist/host/app.js +201 -61
- package/dist/host/index.d.ts +4 -1
- package/dist/host/index.js +1 -0
- package/dist/host/input.d.ts +84 -0
- package/dist/host/input.js +498 -0
- package/dist/host/terminal-host.d.ts +15 -2
- package/dist/host/terminal-host.js +2 -0
- package/dist/index.d.ts +26 -14
- package/dist/index.js +17 -7
- package/dist/node/prompt.d.ts +13 -0
- package/dist/node/prompt.js +30 -9
- package/dist/node/save.d.ts +4 -3
- package/dist/node/save.js +6 -3
- package/dist/node/terminal-host.d.ts +10 -2
- package/dist/node/terminal-host.js +7 -0
- package/dist/node/traceback.d.ts +2 -1
- package/dist/node/traceback.js +3 -39
- package/dist/renderables/align.js +2 -9
- package/dist/renderables/columns.d.ts +45 -29
- package/dist/renderables/columns.js +142 -103
- package/dist/renderables/constrain.js +6 -12
- package/dist/renderables/effect.d.ts +113 -0
- package/dist/renderables/effect.js +182 -0
- package/dist/renderables/effects.d.ts +252 -0
- package/dist/renderables/effects.js +565 -0
- package/dist/renderables/embed.d.ts +94 -31
- package/dist/renderables/embed.js +141 -45
- package/dist/renderables/group.d.ts +6 -3
- package/dist/renderables/group.js +5 -0
- package/dist/renderables/layout.d.ts +6 -23
- package/dist/renderables/layout.js +24 -113
- package/dist/renderables/list-view.d.ts +42 -0
- package/dist/renderables/list-view.js +78 -0
- package/dist/renderables/live.d.ts +40 -16
- package/dist/renderables/live.js +96 -83
- package/dist/renderables/markdown.d.ts +16 -2
- package/dist/renderables/markdown.js +521 -118
- package/dist/renderables/padding.d.ts +22 -1
- package/dist/renderables/padding.js +30 -24
- package/dist/renderables/panel.d.ts +31 -33
- package/dist/renderables/panel.js +137 -141
- package/dist/renderables/progress.d.ts +110 -16
- package/dist/renderables/progress.js +212 -81
- package/dist/renderables/progressBar.d.ts +24 -4
- package/dist/renderables/progressBar.js +78 -23
- package/dist/renderables/prompt.d.ts +37 -15
- package/dist/renderables/prompt.js +125 -85
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +3 -2
- package/dist/renderables/rule.js +50 -52
- package/dist/renderables/spinner.d.ts +28 -2
- package/dist/renderables/spinner.js +52 -19
- package/dist/renderables/status.d.ts +13 -7
- package/dist/renderables/status.js +15 -29
- package/dist/renderables/syntax.d.ts +37 -7
- package/dist/renderables/syntax.js +134 -83
- package/dist/renderables/table.d.ts +58 -20
- package/dist/renderables/table.js +373 -197
- package/dist/renderables/traceback.d.ts +5 -4
- package/dist/renderables/traceback.js +239 -68
- package/dist/renderables/tree.d.ts +10 -2
- package/dist/renderables/tree.js +61 -34
- package/dist/renderables/viewport.d.ts +30 -0
- package/dist/renderables/viewport.js +61 -7
- package/dist/template-bindings/color-funcs.d.ts +31 -9
- package/dist/template-bindings/color-funcs.js +40 -14
- package/dist/template-bindings/helpers.d.ts +4 -2
- package/dist/template-bindings/helpers.js +5 -20
- package/dist/template-bindings/index.d.ts +29 -17
- package/dist/template-bindings/index.js +36 -20
- package/dist/template-bindings/palette-funcs.d.ts +3 -1
- package/dist/template-bindings/palette-funcs.js +11 -7
- package/dist/template-bindings/style-funcs.js +4 -2
- package/dist/themes/buildPalette.d.ts +24 -3
- package/dist/themes/buildPalette.js +52 -8
- package/dist/themes/colorMath.d.ts +29 -17
- package/dist/themes/colorMath.js +74 -48
- package/dist/themes/colorRef.d.ts +2 -17
- package/dist/themes/colorRef.js +13 -31
- package/dist/themes/palette.d.ts +24 -0
- package/dist/themes/palette.js +46 -3
- package/dist/themes/ramp.d.ts +21 -34
- package/dist/themes/ramp.js +23 -42
- package/dist/themes/registry.js +39 -28
- package/dist/widgets/button.d.ts +2 -6
- package/dist/widgets/button.js +9 -40
- package/dist/widgets/checkbox.d.ts +2 -5
- package/dist/widgets/checkbox.js +9 -31
- package/dist/widgets/dropdown.d.ts +2 -5
- package/dist/widgets/dropdown.js +17 -51
- package/dist/widgets/event-router.d.ts +31 -40
- package/dist/widgets/event-router.js +52 -483
- package/dist/widgets/index.d.ts +2 -1
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +2 -5
- package/dist/widgets/slider.js +11 -34
- package/dist/widgets/text-input.d.ts +19 -10
- package/dist/widgets/text-input.js +156 -147
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +2 -5
- package/dist/widgets/toggle.js +8 -31
- package/dist/widgets/types.d.ts +8 -25
- package/dist/widgets/types.js +8 -0
- package/dist/widgets/widget-app.d.ts +24 -7
- package/dist/widgets/widget-app.js +35 -18
- package/dist/widgets/widget-base.d.ts +2 -0
- package/dist/widgets/widget-base.js +9 -2
- package/package.json +31 -17
package/README.md
CHANGED
|
@@ -342,7 +342,7 @@ console.print(new Greeting());
|
|
|
342
342
|
|
|
343
343
|
```typescript
|
|
344
344
|
const console = new Console({
|
|
345
|
-
colorSystem: "truecolor", // null | "auto" | "
|
|
345
|
+
colorSystem: "truecolor", // null | "auto" | "none" | "ansi" | "256" | "truecolor"
|
|
346
346
|
width: 120, // override terminal width
|
|
347
347
|
stderr: true, // write to stderr
|
|
348
348
|
record: true, // record output for export
|
|
@@ -354,14 +354,15 @@ const console = new Console({
|
|
|
354
354
|
When `record: true`, export output after the fact:
|
|
355
355
|
|
|
356
356
|
```typescript
|
|
357
|
-
import {
|
|
357
|
+
import { SOLARIZED_LIGHT, SVG_EXPORT_THEME } from "@promptctl/rich-js";
|
|
358
|
+
import { saveSvg } from "@promptctl/rich-js/node/save";
|
|
358
359
|
|
|
359
|
-
const text = console.exportText();
|
|
360
|
-
const html = console.exportHtml();
|
|
361
|
-
|
|
360
|
+
const text = console.exportText({ clear: false });
|
|
361
|
+
const html = console.exportHtml({ theme: SOLARIZED_LIGHT, clear: false });
|
|
362
|
+
saveSvg(console, "output.svg", { theme: SVG_EXPORT_THEME });
|
|
362
363
|
```
|
|
363
364
|
|
|
364
|
-
`saveText`
|
|
365
|
+
`saveText`, `saveHtml` and `saveSvg` live on the `node/save` subpath because they import `node:fs`; the main barrel stays browser-safe. The [console guide](https://brandon-fryslie.github.io/rich-js/master/console#exporting) covers what each export draws, the themes it takes, and when it clears the recording.
|
|
365
366
|
|
|
366
367
|
## Demos
|
|
367
368
|
|
|
@@ -384,10 +385,12 @@ npm run demo
|
|
|
384
385
|
| `npm run demo-inputs` | rich-config | The widgets — checkbox, toggle, slider, dropdown, text input, button — driving a theme and palette viewer. |
|
|
385
386
|
| `npm run demo:dropdown` | dropdown-demo | Three `Dropdown` widgets: a plain one, one filtered as you type, and one whose options change every few seconds. |
|
|
386
387
|
| `npm run template-bindings` | rich-template-bindings | Type a template on the left and watch it render on the right. |
|
|
387
|
-
| `npm run themes-and-color-studio` | themes-and-color-studio | A printed tour of colours, palettes, bundled themes, and contrast. Prints once and exits; set `EXPORT_HTML=out.html` to save it
|
|
388
|
+
| `npm run themes-and-color-studio` | themes-and-color-studio | A printed tour of colours, palettes, bundled themes, and contrast. Prints once and exits; set any of `EXPORT_HTML=out.html`, `EXPORT_SVG=out.svg` and `EXPORT_TEXT=out.txt` to also save it to each file named. |
|
|
388
389
|
| `npm run strip` | rich-strip | Every built-in `Joiner`, printed side by side. Prints once and exits. |
|
|
390
|
+
| `npm run effects-feel` | effects-feel | Shimmer, a gentle pulse on chosen elements, sparkle, a hue wheel, fade-in and a dissolve-out that eddies back before it goes, on a powerline strip and a run of text, at a colour depth you choose, with keys to step the frame rate, the demo's pace and the theme while it runs. A demo for agreeing how the effects feel; `npm run effects-feel -- --help` lists the flags and keys. |
|
|
389
391
|
| `npm run markup-plugins` | rich-markup-plugins | Custom markup tags registered through `MarkupRegistry`. Prints once and exits. |
|
|
390
|
-
| `npm run viewport` | rich-viewport | A
|
|
392
|
+
| `npm run viewport` | rich-viewport | A selection you move through a list taller than its window with the arrow keys, `j`/`k`, PgUp/PgDn and Home/End, kept in view by `Viewport.ensureVisible`; the mouse wheel scrolls the window, and `q` quits. |
|
|
393
|
+
| `npm run terminal-host` | terminal-host | Each key you press printed as the bytes the terminal sends for it, and each resize as the new size — a program written against `TerminalHost` alone. `examples/terminal-host/embed.ts` runs the same program on an xterm.js terminal in your own page. |
|
|
391
394
|
|
|
392
395
|
`Ctrl-C` quits any demo that stays running.
|
|
393
396
|
|
package/dist/core/box.d.ts
CHANGED
|
@@ -117,7 +117,9 @@ export declare class Box {
|
|
|
117
117
|
plainHeaded(): Box;
|
|
118
118
|
/**
|
|
119
119
|
* [LAW:dataflow-not-control-flow] Every full-width rule the box can draw is
|
|
120
|
-
* this one
|
|
120
|
+
* this one join; which rule it is arrives as four characters, not a branch.
|
|
121
|
+
* The rule is one segment, as the reference's `get_top` is one string, so
|
|
122
|
+
* its bytes are one styled run.
|
|
121
123
|
*/
|
|
122
124
|
private getEdge;
|
|
123
125
|
private getRowChars;
|
package/dist/core/box.js
CHANGED
|
@@ -173,21 +173,14 @@ export class Box {
|
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
175
|
* [LAW:dataflow-not-control-flow] Every full-width rule the box can draw is
|
|
176
|
-
* this one
|
|
176
|
+
* this one join; which rule it is arrives as four characters, not a branch.
|
|
177
|
+
* The rule is one segment, as the reference's `get_top` is one string, so
|
|
178
|
+
* its bytes are one styled run.
|
|
177
179
|
*/
|
|
178
180
|
getEdge(widths, chars, style, edge) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
for (let i = 0; i < widths.length; i++) {
|
|
183
|
-
if (i > 0)
|
|
184
|
-
segments.push(new Segment(chars.cross, style));
|
|
185
|
-
segments.push(new Segment(chars.horizontal.repeat(widths[i]), style));
|
|
186
|
-
}
|
|
187
|
-
if (edge)
|
|
188
|
-
segments.push(new Segment(chars.right, style));
|
|
189
|
-
segments.push(Segment.line());
|
|
190
|
-
return segments;
|
|
181
|
+
const inner = widths.map((width) => chars.horizontal.repeat(width)).join(chars.cross);
|
|
182
|
+
const rule = edge ? chars.left + inner + chars.right : inner;
|
|
183
|
+
return [new Segment(rule, style), Segment.line()];
|
|
191
184
|
}
|
|
192
185
|
getRowChars(level) {
|
|
193
186
|
switch (level) {
|
package/dist/core/cells.d.ts
CHANGED
|
@@ -66,8 +66,19 @@ export declare function setCellSize(text: string, totalWidth: CellCol): string;
|
|
|
66
66
|
* Splits text at a cell position. Returns [left, right].
|
|
67
67
|
* When the position falls mid-wide-character, the left side is padded
|
|
68
68
|
* to reach exactly `position` cells. The wide char remains in the right side.
|
|
69
|
+
*
|
|
70
|
+
* [LAW:one-source-of-truth] The left side is `cellFit`'s, so the cut falls
|
|
71
|
+
* between the grapheme clusters `cellLen` measures and never inside one.
|
|
69
72
|
*/
|
|
70
73
|
export declare function splitText(text: string, position: CellCol): [string, string];
|
|
74
|
+
/**
|
|
75
|
+
* Cuts `text` into exactly `position` cells and the rest, as the reference's
|
|
76
|
+
* `Segment.split_cells` does: a wide glyph the cut goes through fits neither
|
|
77
|
+
* side, so it leaves a space in each of its cells, where `splitText` keeps it
|
|
78
|
+
* whole on the right. Every other character lands on exactly one side.
|
|
79
|
+
* `position` must fall inside `text`, between 0 and its width.
|
|
80
|
+
*/
|
|
81
|
+
export declare function cutCells(text: string, position: CellCol): [string, string];
|
|
71
82
|
/**
|
|
72
83
|
* Wraps text into lines of at most `maxWidth` cells, preserving every code
|
|
73
84
|
* point: `chopCells(t, w).join("") === t`. Unlike `splitText` this never pads,
|
|
@@ -79,6 +90,12 @@ export declare function splitText(text: string, position: CellCol): [string, str
|
|
|
79
90
|
* or less is no budget at all, so the text comes back as one line.
|
|
80
91
|
*/
|
|
81
92
|
export declare function chopCells(text: string, maxWidth: CellCol): string[];
|
|
93
|
+
/**
|
|
94
|
+
* `line` split at each of `cuts`, in one walk over it. Converting each cut to a
|
|
95
|
+
* code-unit offset on its own rescans the line from the start every time, which
|
|
96
|
+
* is quadratic in a long enough string.
|
|
97
|
+
*/
|
|
98
|
+
export declare function splitAtCells(line: string, cuts: readonly CellCol[]): string[];
|
|
82
99
|
/**
|
|
83
100
|
* The grapheme clusters of `text`, in order: the unit `string-width` measures,
|
|
84
101
|
* and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
|
|
@@ -105,8 +122,8 @@ export declare function cellFitEnd(text: string, cap: CellCol): string;
|
|
|
105
122
|
* offset without slicing the tail, avoiding O(N²) allocation when called
|
|
106
123
|
* repeatedly across a long string.
|
|
107
124
|
*
|
|
108
|
-
* [LAW:types-are-the-program] returns CodePoint because
|
|
109
|
-
*
|
|
125
|
+
* [LAW:types-are-the-program] returns CodePoint because it stops between
|
|
126
|
+
* grapheme clusters, and every cluster boundary is a code-point boundary.
|
|
110
127
|
*/
|
|
111
128
|
export declare function cellFitFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
|
|
112
129
|
/**
|
|
@@ -123,37 +140,53 @@ export declare function cellFitFrom(text: string, startCU: CodePoint, cap: CellC
|
|
|
123
140
|
*/
|
|
124
141
|
export declare function cellStepFrom(text: string, startCU: CodePoint, cap: CellCol): CodePoint;
|
|
125
142
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* Clamps to `content.length` if `cellCol` exceeds the string's total
|
|
130
|
-
* cell width.
|
|
131
|
-
*
|
|
132
|
-
* This is the inverse of `cellLen(content.slice(0, codeUnit))` — given a
|
|
133
|
-
* visual column, return the corresponding string index.
|
|
134
|
-
*
|
|
135
|
-
* Returns `CodePoint` because `for...of` iteration always stops on a
|
|
136
|
-
* code-point boundary.
|
|
143
|
+
* The code-unit offset a visual column falls at: the end of the largest
|
|
144
|
+
* prefix of `content` within `cellCol` cells, so a column inside a wide glyph
|
|
145
|
+
* lands before it. The inverse of `cellLen(content.slice(0, offset))`.
|
|
137
146
|
*/
|
|
138
147
|
export declare function cellColToCodeUnitOffset(content: string, cellCol: CellCol): CodePoint;
|
|
139
148
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
* The offset just past the grapheme cluster at `cu`: one step of a cursor, a
|
|
150
|
+
* Delete, a transposition, so `👍🏽` or `👨👩👧` moves and goes as the one
|
|
151
|
+
* glyph `cellLen` measures, never a code point at a time. `cu` is a cluster
|
|
152
|
+
* boundary in `s` (see `graphemeBoundary`); `s.length` gives `s.length`.
|
|
153
|
+
*/
|
|
154
|
+
export declare function nextGrapheme(s: string, cu: CodePoint): CodePoint;
|
|
155
|
+
/** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
|
|
156
|
+
export declare function prevGrapheme(s: string, cu: CodePoint): CodePoint;
|
|
157
|
+
/**
|
|
158
|
+
* Where an offset into `s` comes to rest as a cursor: itself when it is a
|
|
159
|
+
* cluster boundary, else the end of the cluster it falls inside, and never
|
|
160
|
+
* past `s.length`. The one parse from any number to the boundaries
|
|
161
|
+
* `nextGrapheme` and `prevGrapheme` step between.
|
|
162
|
+
*/
|
|
163
|
+
export declare function graphemeBoundary(s: string, cu: CodeUnit): CodePoint;
|
|
164
|
+
declare const _tabSize: unique symbol;
|
|
165
|
+
/** Cells between tab stops: a positive integer, so every stop is a column. */
|
|
166
|
+
export type TabSize = number & {
|
|
167
|
+
readonly [_tabSize]: true;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* `n` as a `TabSize`, or a `RangeError` naming it. Rich divides by the tab
|
|
171
|
+
* size and raises on 0; here the value is refused where it is given, not at
|
|
172
|
+
* the first tab it meets.
|
|
147
173
|
*/
|
|
148
|
-
export declare function
|
|
174
|
+
export declare function parseTabSize(n: number): TabSize;
|
|
175
|
+
/** Text with its tabs expanded, and where each offset into the original went. */
|
|
176
|
+
export interface TabExpansion {
|
|
177
|
+
readonly text: string;
|
|
178
|
+
offsetOf(offset: number): number;
|
|
179
|
+
}
|
|
149
180
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
181
|
+
* `text` with each tab widened to the next tab stop — a multiple of `tabSize`
|
|
182
|
+
* cells from the start of its line — as Rich's `Text.expand_tabs` widens it, and
|
|
183
|
+
* `offsetOf`, where each code-unit offset into `text` lands in the result.
|
|
153
184
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
185
|
+
* A tab is a column position, not a run of spaces: one that already sits a
|
|
186
|
+
* cell short of a stop widens to a single space. `offsetOf` is there because a
|
|
187
|
+
* caller that annotates the text by offset — a `RichText`'s spans — has to move
|
|
188
|
+
* its offsets with the characters, or every annotation after a tab lands short
|
|
189
|
+
* of what it styled.
|
|
157
190
|
*/
|
|
158
|
-
export declare function
|
|
191
|
+
export declare function expandTabs(text: string, tabSize: TabSize): TabExpansion;
|
|
159
192
|
export {};
|
package/dist/core/cells.js
CHANGED
|
@@ -3,28 +3,17 @@
|
|
|
3
3
|
* Handles ASCII, CJK (double-width), and emoji characters.
|
|
4
4
|
*/
|
|
5
5
|
import stringWidth from "string-width";
|
|
6
|
+
import { Memo } from "./memo.js";
|
|
6
7
|
// [LAW:one-source-of-truth] string-width is the single authority for cell width
|
|
7
|
-
const
|
|
8
|
-
const CACHE_MAX = 4096;
|
|
8
|
+
const cellLenMemo = new Memo();
|
|
9
9
|
/**
|
|
10
10
|
* Returns the terminal cell width of a string.
|
|
11
11
|
*/
|
|
12
12
|
export function cellLen(text) {
|
|
13
13
|
if (text.length === 0)
|
|
14
14
|
return 0;
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
const cached = cellLenCache.get(text);
|
|
18
|
-
if (cached !== undefined)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
|
-
const width = stringWidth(text);
|
|
22
|
-
if (text.length <= 64) {
|
|
23
|
-
if (cellLenCache.size >= CACHE_MAX)
|
|
24
|
-
cellLenCache.clear();
|
|
25
|
-
cellLenCache.set(text, width);
|
|
26
|
-
}
|
|
27
|
-
return width;
|
|
15
|
+
// Short strings recur (words, cells, labels); a long one is rarely measured twice.
|
|
16
|
+
return text.length <= 64 ? cellLenMemo.get(text, stringWidth) : stringWidth(text);
|
|
28
17
|
}
|
|
29
18
|
/** Brand a raw number as a CellCol. Use only at trust boundaries. */
|
|
30
19
|
export function asCellCol(n) { return n; }
|
|
@@ -74,36 +63,41 @@ export function setCellSize(text, totalWidth) {
|
|
|
74
63
|
if (currentWidth < totalWidth) {
|
|
75
64
|
return text + " ".repeat(totalWidth - currentWidth);
|
|
76
65
|
}
|
|
77
|
-
return
|
|
66
|
+
return splitText(text, totalWidth)[0];
|
|
78
67
|
}
|
|
79
68
|
/**
|
|
80
69
|
* Splits text at a cell position. Returns [left, right].
|
|
81
70
|
* When the position falls mid-wide-character, the left side is padded
|
|
82
71
|
* to reach exactly `position` cells. The wide char remains in the right side.
|
|
72
|
+
*
|
|
73
|
+
* [LAW:one-source-of-truth] The left side is `cellFit`'s, so the cut falls
|
|
74
|
+
* between the grapheme clusters `cellLen` measures and never inside one.
|
|
83
75
|
*/
|
|
84
76
|
export function splitText(text, position) {
|
|
85
77
|
if (position <= 0)
|
|
86
78
|
return ["", text];
|
|
87
|
-
|
|
88
|
-
if (position >= totalWidth)
|
|
79
|
+
if (position >= cellLen(text))
|
|
89
80
|
return [text, ""];
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return [
|
|
81
|
+
const left = cellFit(text, position);
|
|
82
|
+
return [left + " ".repeat(position - cellLen(left)), text.slice(left.length)];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Cuts `text` into exactly `position` cells and the rest, as the reference's
|
|
86
|
+
* `Segment.split_cells` does: a wide glyph the cut goes through fits neither
|
|
87
|
+
* side, so it leaves a space in each of its cells, where `splitText` keeps it
|
|
88
|
+
* whole on the right. Every other character lands on exactly one side.
|
|
89
|
+
* `position` must fall inside `text`, between 0 and its width.
|
|
90
|
+
*/
|
|
91
|
+
export function cutCells(text, position) {
|
|
92
|
+
const head = cellFit(text, position);
|
|
93
|
+
// Cells of the glyph the cut goes through that lie left of it; 0 when the
|
|
94
|
+
// cut falls between glyphs, and then there is no such glyph.
|
|
95
|
+
const straddle = position - cellLen(head);
|
|
96
|
+
const [glyph = ""] = straddle > 0 ? clustersFrom(text, asCodePoint(head.length)) : [];
|
|
97
|
+
return [
|
|
98
|
+
head + " ".repeat(straddle),
|
|
99
|
+
" ".repeat(cellLen(glyph) - straddle) + text.slice(head.length + glyph.length),
|
|
100
|
+
];
|
|
107
101
|
}
|
|
108
102
|
/**
|
|
109
103
|
* Wraps text into lines of at most `maxWidth` cells, preserving every code
|
|
@@ -127,8 +121,39 @@ export function chopCells(text, maxWidth) {
|
|
|
127
121
|
}
|
|
128
122
|
return lines;
|
|
129
123
|
}
|
|
130
|
-
|
|
124
|
+
/**
|
|
125
|
+
* `line` split at each of `cuts`, in one walk over it. Converting each cut to a
|
|
126
|
+
* code-unit offset on its own rescans the line from the start every time, which
|
|
127
|
+
* is quadratic in a long enough string.
|
|
128
|
+
*/
|
|
129
|
+
export function splitAtCells(line, cuts) {
|
|
130
|
+
const pieces = [];
|
|
131
|
+
let start = 0;
|
|
132
|
+
let offset = 0;
|
|
133
|
+
let cells = 0;
|
|
134
|
+
for (const ch of graphemes(line)) {
|
|
135
|
+
if (pieces.length < cuts.length && cells >= cuts[pieces.length]) {
|
|
136
|
+
pieces.push(line.slice(start, offset));
|
|
137
|
+
start = offset;
|
|
138
|
+
}
|
|
139
|
+
cells += cellLen(ch);
|
|
140
|
+
offset += ch.length;
|
|
141
|
+
}
|
|
142
|
+
return [...pieces, line.slice(start)];
|
|
143
|
+
}
|
|
144
|
+
// [LAW:no-shared-mutable-globals] Private memos, written only by `segmentsOf`:
|
|
145
|
+
// the segmenter, and the segmentation of the last string walked, so a caller
|
|
146
|
+
// stepping along one string does not re-segment the whole of it every step.
|
|
131
147
|
let segmenter;
|
|
148
|
+
let lastSegmented;
|
|
149
|
+
/** The grapheme segmentation of `text`, reusing the last one when it is the same string. */
|
|
150
|
+
function segmentsOf(text) {
|
|
151
|
+
if (lastSegmented?.text !== text) {
|
|
152
|
+
segmenter ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
153
|
+
lastSegmented = { text, segments: segmenter.segment(text) };
|
|
154
|
+
}
|
|
155
|
+
return lastSegmented.segments;
|
|
156
|
+
}
|
|
132
157
|
/**
|
|
133
158
|
* The grapheme clusters of `text`, in order: the unit `string-width` measures,
|
|
134
159
|
* and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
|
|
@@ -137,8 +162,19 @@ let segmenter;
|
|
|
137
162
|
* between two of its code points leaves half a glyph.
|
|
138
163
|
*/
|
|
139
164
|
export function graphemes(text) {
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
return Array.from(clustersFrom(text, asCodePoint(0)));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The clusters of `text` from code unit `start` on, lazily, so a walk that
|
|
169
|
+
* stops early pays only for the clusters it reads.
|
|
170
|
+
*/
|
|
171
|
+
function* clustersFrom(text, start) {
|
|
172
|
+
const segments = segmentsOf(text);
|
|
173
|
+
for (let at = start, s = segments.containing(at); s; s = segments.containing(at)) {
|
|
174
|
+
const end = s.index + s.segment.length;
|
|
175
|
+
yield text.slice(at, end);
|
|
176
|
+
at = asCodePoint(end);
|
|
177
|
+
}
|
|
142
178
|
}
|
|
143
179
|
/**
|
|
144
180
|
* Returns the largest prefix of `text` whose cell width fits within `cap` cells,
|
|
@@ -150,13 +186,14 @@ export function graphemes(text) {
|
|
|
150
186
|
* must decide whether to force-take the glyph or skip it).
|
|
151
187
|
*/
|
|
152
188
|
export function cellFit(text, cap) {
|
|
153
|
-
return
|
|
189
|
+
return text.slice(0, fitLength(clustersFrom(text, asCodePoint(0)), cap));
|
|
154
190
|
}
|
|
155
191
|
/** `cellFit` from the other end: the largest suffix of `text` within `cap` cells. */
|
|
156
192
|
export function cellFitEnd(text, cap) {
|
|
157
|
-
return
|
|
193
|
+
return text.slice(text.length - fitLength(graphemes(text).reverse(), cap));
|
|
158
194
|
}
|
|
159
|
-
|
|
195
|
+
/** The code units of the leading `clusters` that fit within `cap` cells. */
|
|
196
|
+
function fitLength(clusters, cap) {
|
|
160
197
|
let w = 0;
|
|
161
198
|
let n = 0;
|
|
162
199
|
for (const cluster of clusters) {
|
|
@@ -164,9 +201,9 @@ function fitClusters(clusters, cap) {
|
|
|
164
201
|
if (w + cw > cap)
|
|
165
202
|
break;
|
|
166
203
|
w += cw;
|
|
167
|
-
n
|
|
204
|
+
n += cluster.length;
|
|
168
205
|
}
|
|
169
|
-
return
|
|
206
|
+
return n;
|
|
170
207
|
}
|
|
171
208
|
/**
|
|
172
209
|
* Returns the largest code-unit end offset starting from `startCU` whose
|
|
@@ -174,22 +211,11 @@ function fitClusters(clusters, cap) {
|
|
|
174
211
|
* offset without slicing the tail, avoiding O(N²) allocation when called
|
|
175
212
|
* repeatedly across a long string.
|
|
176
213
|
*
|
|
177
|
-
* [LAW:types-are-the-program] returns CodePoint because
|
|
178
|
-
*
|
|
214
|
+
* [LAW:types-are-the-program] returns CodePoint because it stops between
|
|
215
|
+
* grapheme clusters, and every cluster boundary is a code-point boundary.
|
|
179
216
|
*/
|
|
180
217
|
export function cellFitFrom(text, startCU, cap) {
|
|
181
|
-
|
|
182
|
-
let i = startCU;
|
|
183
|
-
while (i < text.length) {
|
|
184
|
-
const cp = text.codePointAt(i);
|
|
185
|
-
const ch = String.fromCodePoint(cp);
|
|
186
|
-
const cw = cellLen(ch);
|
|
187
|
-
if (w + cw > cap)
|
|
188
|
-
break;
|
|
189
|
-
w += cw;
|
|
190
|
-
i = asCodePoint(i + ch.length);
|
|
191
|
-
}
|
|
192
|
-
return i;
|
|
218
|
+
return asCodePoint(startCU + fitLength(clustersFrom(text, startCU), cap));
|
|
193
219
|
}
|
|
194
220
|
/**
|
|
195
221
|
* Like `cellFitFrom`, but for any `startCU` inside `text` the result is
|
|
@@ -204,84 +230,105 @@ export function cellFitFrom(text, startCU, cap) {
|
|
|
204
230
|
* `chopCells` did neither and hung.
|
|
205
231
|
*/
|
|
206
232
|
export function cellStepFrom(text, startCU, cap) {
|
|
207
|
-
|
|
233
|
+
const [first = ""] = clustersFrom(text, startCU);
|
|
234
|
+
return asCodePoint(Math.max(cellFitFrom(text, startCU, cap), startCU + first.length));
|
|
208
235
|
}
|
|
209
236
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
* Clamps to `content.length` if `cellCol` exceeds the string's total
|
|
214
|
-
* cell width.
|
|
215
|
-
*
|
|
216
|
-
* This is the inverse of `cellLen(content.slice(0, codeUnit))` — given a
|
|
217
|
-
* visual column, return the corresponding string index.
|
|
218
|
-
*
|
|
219
|
-
* Returns `CodePoint` because `for...of` iteration always stops on a
|
|
220
|
-
* code-point boundary.
|
|
237
|
+
* The code-unit offset a visual column falls at: the end of the largest
|
|
238
|
+
* prefix of `content` within `cellCol` cells, so a column inside a wide glyph
|
|
239
|
+
* lands before it. The inverse of `cellLen(content.slice(0, offset))`.
|
|
221
240
|
*/
|
|
222
241
|
export function cellColToCodeUnitOffset(content, cellCol) {
|
|
223
|
-
|
|
224
|
-
let i = 0;
|
|
225
|
-
for (const ch of content) {
|
|
226
|
-
if (w >= cellCol)
|
|
227
|
-
break;
|
|
228
|
-
const cw = cellLen(ch);
|
|
229
|
-
if (w + cw > cellCol)
|
|
230
|
-
break;
|
|
231
|
-
w += cw;
|
|
232
|
-
i += ch.length;
|
|
233
|
-
}
|
|
234
|
-
return asCodePoint(i);
|
|
242
|
+
return cellFitFrom(content, asCodePoint(0), cellCol);
|
|
235
243
|
}
|
|
236
244
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* Handles surrogate pairs: when the code point at `cu` is a supplementary
|
|
242
|
-
* character (U+10000…U+10FFFF) it occupies 2 UTF-16 code units, so the
|
|
243
|
-
* returned offset advances by 2.
|
|
245
|
+
* The offset just past the grapheme cluster at `cu`: one step of a cursor, a
|
|
246
|
+
* Delete, a transposition, so `👍🏽` or `👨👩👧` moves and goes as the one
|
|
247
|
+
* glyph `cellLen` measures, never a code point at a time. `cu` is a cluster
|
|
248
|
+
* boundary in `s` (see `graphemeBoundary`); `s.length` gives `s.length`.
|
|
244
249
|
*/
|
|
245
|
-
export function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const cp = s.codePointAt(cu);
|
|
249
|
-
return asCodePoint(cu + (cp > 0xFFFF ? 2 : 1));
|
|
250
|
+
export function nextGrapheme(s, cu) {
|
|
251
|
+
const [cluster = ""] = clustersFrom(s, cu);
|
|
252
|
+
return asCodePoint(cu + cluster.length);
|
|
250
253
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
* offset of the start of the *previous* code point. Returns 0 when already
|
|
254
|
-
* at the start.
|
|
255
|
-
*
|
|
256
|
-
* Handles surrogate pairs: when the code unit at `cu - 1` is a low surrogate
|
|
257
|
-
* AND the code unit at `cu - 2` is a high surrogate, steps back 2 code units.
|
|
258
|
-
* Unpaired surrogates are treated as 1-CU characters.
|
|
259
|
-
*/
|
|
260
|
-
export function prevCodePoint(s, cu) {
|
|
254
|
+
/** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
|
|
255
|
+
export function prevGrapheme(s, cu) {
|
|
261
256
|
if (cu <= 0)
|
|
262
257
|
return asCodePoint(0);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
258
|
+
return asCodePoint(segmentsOf(s).containing(cu - 1).index);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Where an offset into `s` comes to rest as a cursor: itself when it is a
|
|
262
|
+
* cluster boundary, else the end of the cluster it falls inside, and never
|
|
263
|
+
* past `s.length`. The one parse from any number to the boundaries
|
|
264
|
+
* `nextGrapheme` and `prevGrapheme` step between.
|
|
265
|
+
*/
|
|
266
|
+
export function graphemeBoundary(s, cu) {
|
|
267
|
+
const at = Math.min(cu, s.length);
|
|
268
|
+
const cluster = segmentsOf(s).containing(at);
|
|
269
|
+
return asCodePoint(cluster === undefined || cluster.index === at ? at : cluster.index + cluster.segment.length);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `n` as a `TabSize`, or a `RangeError` naming it. Rich divides by the tab
|
|
273
|
+
* size and raises on 0; here the value is refused where it is given, not at
|
|
274
|
+
* the first tab it meets.
|
|
275
|
+
*/
|
|
276
|
+
export function parseTabSize(n) {
|
|
277
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
278
|
+
throw new RangeError(`tabSize must be a positive integer, got ${n}`);
|
|
268
279
|
}
|
|
269
|
-
return
|
|
280
|
+
return n;
|
|
270
281
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
+
/**
|
|
283
|
+
* `text` with each tab widened to the next tab stop — a multiple of `tabSize`
|
|
284
|
+
* cells from the start of its line — as Rich's `Text.expand_tabs` widens it, and
|
|
285
|
+
* `offsetOf`, where each code-unit offset into `text` lands in the result.
|
|
286
|
+
*
|
|
287
|
+
* A tab is a column position, not a run of spaces: one that already sits a
|
|
288
|
+
* cell short of a stop widens to a single space. `offsetOf` is there because a
|
|
289
|
+
* caller that annotates the text by offset — a `RichText`'s spans — has to move
|
|
290
|
+
* its offsets with the characters, or every annotation after a tab lands short
|
|
291
|
+
* of what it styled.
|
|
292
|
+
*/
|
|
293
|
+
export function expandTabs(text, tabSize) {
|
|
294
|
+
const [head, ...rest] = text.split("\t");
|
|
295
|
+
const tabAt = []; // offset of each tab in `text`
|
|
296
|
+
const grownBy = []; // code units added by this tab and every one before it
|
|
297
|
+
const columnAfter = (piece, column) => {
|
|
298
|
+
const newline = piece.lastIndexOf("\n");
|
|
299
|
+
return (newline < 0 ? column : 0) + cellLen(piece.slice(newline + 1));
|
|
300
|
+
};
|
|
301
|
+
// A piece's column is read only when a tab follows it, so text with no tab
|
|
302
|
+
// costs one `split` and no measuring.
|
|
303
|
+
let expanded = head;
|
|
304
|
+
let before = head; // the piece the next tab follows
|
|
305
|
+
let column = 0; // the column `before` starts at
|
|
306
|
+
let offset = head.length;
|
|
307
|
+
let grown = 0;
|
|
308
|
+
for (const piece of rest) {
|
|
309
|
+
column = columnAfter(before, column);
|
|
310
|
+
const spaces = tabSize - (column % tabSize);
|
|
311
|
+
grown += spaces - 1;
|
|
312
|
+
tabAt.push(offset);
|
|
313
|
+
grownBy.push(grown);
|
|
314
|
+
expanded += " ".repeat(spaces) + piece;
|
|
315
|
+
column += spaces;
|
|
316
|
+
before = piece;
|
|
317
|
+
offset += 1 + piece.length;
|
|
282
318
|
}
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
const
|
|
286
|
-
|
|
319
|
+
// An offset moves by what every tab before it grew: the count of tabs at
|
|
320
|
+
// offsets below it, found by bisection since `tabAt` ascends.
|
|
321
|
+
const offsetOf = (at) => {
|
|
322
|
+
let low = 0;
|
|
323
|
+
let high = tabAt.length;
|
|
324
|
+
while (low < high) {
|
|
325
|
+
const mid = (low + high) >> 1;
|
|
326
|
+
if (tabAt[mid] < at)
|
|
327
|
+
low = mid + 1;
|
|
328
|
+
else
|
|
329
|
+
high = mid;
|
|
330
|
+
}
|
|
331
|
+
return at + (low === 0 ? 0 : grownBy[low - 1]);
|
|
332
|
+
};
|
|
333
|
+
return { text: expanded, offsetOf };
|
|
287
334
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock — the time source and timer whoever draws frames is handed, and the
|
|
3
|
+
* frame rate it ticks at.
|
|
4
|
+
*
|
|
5
|
+
* [LAW:no-ambient-temporal-coupling] Frame rate belongs to whoever draws the
|
|
6
|
+
* frames — an `App`, a `Live` — never to what is drawn, and each ticks on the
|
|
7
|
+
* `Clock` it was given. An `App` reads the frame's time through that clock and
|
|
8
|
+
* hands it to its view as data, so nothing rendered reads a clock, and an
|
|
9
|
+
* effect sampled thirty times a second or once every two seconds runs the
|
|
10
|
+
* same code.
|
|
11
|
+
*
|
|
12
|
+
* [LAW:effects-at-boundaries] The clock is a capability passed in, like the
|
|
13
|
+
* `TerminalHost`: a test hands over a clock whose time it moves by hand and
|
|
14
|
+
* counts frames with no real timer, and nothing it starts can outlive the
|
|
15
|
+
* thing that started it. `systemClock` is the one place the library reads the
|
|
16
|
+
* platform's clock and timers, and it reads them only when called.
|
|
17
|
+
*/
|
|
18
|
+
import type { Unsubscribe } from "./subscription.js";
|
|
19
|
+
declare const parsed: unique symbol;
|
|
20
|
+
/**
|
|
21
|
+
* How often frames are drawn: a positive, finite number of frames a second,
|
|
22
|
+
* fractional included — 0.5 is one frame every two seconds.
|
|
23
|
+
*
|
|
24
|
+
* [LAW:parse-dont-validate] Only `frameRate` makes one, so holding a
|
|
25
|
+
* `FrameRate` is the proof the number was checked, and nothing that takes one
|
|
26
|
+
* checks it again.
|
|
27
|
+
*/
|
|
28
|
+
export interface FrameRate {
|
|
29
|
+
readonly perSecond: number;
|
|
30
|
+
/** Seconds from one frame to the next. */
|
|
31
|
+
readonly interval: number;
|
|
32
|
+
readonly [parsed]: true;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* `perSecond` as a `FrameRate`; anything but a positive, finite number throws,
|
|
36
|
+
* and so does a rate whose frames are further apart than a timer can wait —
|
|
37
|
+
* about 24.8 days.
|
|
38
|
+
*/
|
|
39
|
+
export declare function frameRate(perSecond: number): FrameRate;
|
|
40
|
+
export interface Clock {
|
|
41
|
+
/** The time now, in seconds, on this clock's own timeline, which never runs backwards. */
|
|
42
|
+
now(): number;
|
|
43
|
+
/** Call `tick` once a frame at `rate` until the returned function is called. */
|
|
44
|
+
every(rate: FrameRate, tick: () => void): Unsubscribe;
|
|
45
|
+
}
|
|
46
|
+
/** The platform's monotonic clock and its interval timer. */
|
|
47
|
+
export declare function systemClock(): Clock;
|
|
48
|
+
export {};
|