@promptctl/rich-js 0.18.0 → 0.19.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 +9 -7
- package/dist/core/anchor.d.ts +45 -0
- package/dist/core/anchor.js +39 -0
- package/dist/core/ansi.d.ts +65 -0
- package/dist/core/ansi.js +231 -0
- package/dist/core/box.d.ts +145 -0
- package/dist/core/box.js +372 -0
- package/dist/core/cells.d.ts +192 -0
- package/dist/core/cells.js +334 -0
- package/dist/core/clock.d.ts +48 -0
- package/dist/core/clock.js +42 -0
- package/dist/core/color.d.ts +326 -0
- package/dist/core/color.js +1225 -0
- package/dist/core/console.d.ts +196 -0
- package/dist/core/console.js +596 -0
- package/dist/core/easing.d.ts +86 -0
- package/dist/core/easing.js +171 -0
- package/dist/core/emoji.d.ts +24 -0
- package/dist/core/emoji.js +391 -0
- package/dist/core/env.d.ts +16 -0
- package/dist/core/env.js +16 -0
- package/dist/core/export-html.d.ts +59 -0
- package/dist/core/export-html.js +197 -0
- package/dist/core/export-lines.d.ts +130 -0
- package/dist/core/export-lines.js +159 -0
- 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 +33 -0
- package/dist/core/highlighter.js +190 -0
- package/dist/core/json.d.ts +27 -0
- package/dist/core/json.js +53 -0
- package/dist/core/markup.d.ts +140 -0
- package/dist/core/markup.js +654 -0
- package/dist/core/measure.d.ts +27 -0
- package/dist/core/measure.js +67 -0
- package/dist/core/memo.d.ts +19 -0
- package/dist/core/memo.js +34 -0
- package/dist/core/oklch.d.ts +161 -0
- package/dist/core/oklch.js +312 -0
- package/dist/core/osc8.d.ts +68 -0
- package/dist/core/osc8.js +78 -0
- package/dist/core/paint.d.ts +107 -0
- package/dist/core/paint.js +150 -0
- package/dist/core/place.d.ts +30 -0
- package/dist/core/place.js +49 -0
- package/dist/core/pretty.d.ts +247 -0
- package/dist/core/pretty.js +906 -0
- package/dist/core/protocol.d.ts +305 -0
- package/dist/core/protocol.js +183 -0
- package/dist/core/render.d.ts +87 -0
- package/dist/core/render.js +105 -0
- package/dist/core/segment.d.ts +162 -0
- package/dist/core/segment.js +488 -0
- package/dist/core/spinnerData.d.ts +12 -0
- package/dist/core/spinnerData.js +117 -0
- package/dist/core/strip.d.ts +152 -0
- package/dist/core/strip.js +326 -0
- package/dist/core/style.d.ts +189 -0
- package/dist/core/style.js +758 -0
- package/dist/core/subscription.d.ts +12 -0
- package/dist/core/subscription.js +12 -0
- package/dist/core/text.d.ts +283 -0
- package/dist/core/text.js +1097 -0
- package/dist/core/wrap.d.ts +46 -0
- package/dist/core/wrap.js +140 -0
- package/dist/host/app.d.ts +133 -0
- package/dist/host/app.js +285 -0
- package/dist/host/host-environment.d.ts +26 -0
- package/dist/host/host-environment.js +44 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +3 -0
- package/dist/host/terminal-host.d.ts +180 -0
- package/dist/host/terminal-host.js +186 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.js +114 -0
- package/dist/node/prompt.d.ts +34 -0
- package/dist/node/prompt.js +51 -0
- package/dist/node/save.d.ts +18 -0
- package/dist/node/save.js +24 -0
- package/dist/node/terminal-host.d.ts +114 -0
- package/dist/node/terminal-host.js +227 -0
- package/dist/node/traceback.d.ts +38 -0
- package/dist/node/traceback.js +98 -0
- package/dist/renderables/align.d.ts +16 -0
- package/dist/renderables/align.js +26 -0
- package/dist/renderables/columns.d.ts +77 -0
- package/dist/renderables/columns.js +191 -0
- package/dist/renderables/constrain.d.ts +15 -0
- package/dist/renderables/constrain.js +31 -0
- package/dist/renderables/effect.d.ts +98 -0
- package/dist/renderables/effect.js +160 -0
- package/dist/renderables/embed.d.ts +107 -0
- package/dist/renderables/embed.js +160 -0
- package/dist/renderables/flexStrip.d.ts +41 -0
- package/dist/renderables/flexStrip.js +200 -0
- package/dist/renderables/group.d.ts +14 -0
- package/dist/renderables/group.js +22 -0
- package/dist/renderables/layout.d.ts +75 -0
- package/dist/renderables/layout.js +223 -0
- package/dist/renderables/live.d.ts +74 -0
- package/dist/renderables/live.js +188 -0
- package/dist/renderables/markdown.d.ts +38 -0
- package/dist/renderables/markdown.js +587 -0
- package/dist/renderables/padding.d.ts +59 -0
- package/dist/renderables/padding.js +147 -0
- package/dist/renderables/panel.d.ts +114 -0
- package/dist/renderables/panel.js +303 -0
- package/dist/renderables/progress.d.ts +178 -0
- package/dist/renderables/progress.js +362 -0
- package/dist/renderables/progressBar.d.ts +31 -0
- package/dist/renderables/progressBar.js +64 -0
- package/dist/renderables/prompt.d.ts +71 -0
- package/dist/renderables/prompt.js +165 -0
- package/dist/renderables/ratio.d.ts +123 -0
- package/dist/renderables/ratio.js +213 -0
- package/dist/renderables/rule.d.ts +26 -0
- package/dist/renderables/rule.js +86 -0
- package/dist/renderables/spinner.d.ts +57 -0
- package/dist/renderables/spinner.js +103 -0
- package/dist/renderables/status.d.ts +29 -0
- package/dist/renderables/status.js +46 -0
- package/dist/renderables/syntax.d.ts +65 -0
- package/dist/renderables/syntax.js +166 -0
- package/dist/renderables/table.d.ts +232 -0
- package/dist/renderables/table.js +973 -0
- package/dist/renderables/traceback.d.ts +32 -0
- package/dist/renderables/traceback.js +279 -0
- package/dist/renderables/tree.d.ts +76 -0
- package/dist/renderables/tree.js +177 -0
- package/dist/renderables/viewport.d.ts +114 -0
- package/dist/renderables/viewport.js +196 -0
- package/dist/template-bindings/color-funcs.d.ts +109 -0
- package/dist/template-bindings/color-funcs.js +243 -0
- package/dist/template-bindings/helpers.d.ts +26 -0
- package/dist/template-bindings/helpers.js +33 -0
- package/dist/template-bindings/index.d.ts +119 -0
- package/dist/template-bindings/index.js +162 -0
- package/dist/template-bindings/palette-funcs.d.ts +96 -0
- package/dist/template-bindings/palette-funcs.js +145 -0
- package/dist/template-bindings/style-funcs.d.ts +28 -0
- package/dist/template-bindings/style-funcs.js +157 -0
- package/dist/themes/buildPalette.d.ts +57 -0
- package/dist/themes/buildPalette.js +100 -0
- package/dist/themes/colorMath.d.ts +94 -0
- package/dist/themes/colorMath.js +280 -0
- package/dist/themes/colorRef.d.ts +36 -0
- package/dist/themes/colorRef.js +78 -0
- package/dist/themes/data/ansi-tables.d.ts +14 -0
- package/dist/themes/data/ansi-tables.js +49 -0
- package/dist/themes/data/atom-one-dark.d.ts +3 -0
- package/dist/themes/data/atom-one-dark.js +177 -0
- package/dist/themes/data/atom-one-light.d.ts +3 -0
- package/dist/themes/data/atom-one-light.js +177 -0
- package/dist/themes/data/catppuccin-frappe.d.ts +3 -0
- package/dist/themes/data/catppuccin-frappe.js +177 -0
- package/dist/themes/data/catppuccin-latte.d.ts +3 -0
- package/dist/themes/data/catppuccin-latte.js +177 -0
- package/dist/themes/data/catppuccin-macchiato.d.ts +3 -0
- package/dist/themes/data/catppuccin-macchiato.js +177 -0
- package/dist/themes/data/catppuccin-mocha.d.ts +3 -0
- package/dist/themes/data/catppuccin-mocha.js +177 -0
- package/dist/themes/data/cyberpunk.d.ts +17 -0
- package/dist/themes/data/cyberpunk.js +193 -0
- package/dist/themes/data/default.d.ts +3 -0
- package/dist/themes/data/default.js +160 -0
- package/dist/themes/data/dracula.d.ts +3 -0
- package/dist/themes/data/dracula.js +177 -0
- package/dist/themes/data/flexoki.d.ts +3 -0
- package/dist/themes/data/flexoki.js +177 -0
- package/dist/themes/data/gruvbox.d.ts +3 -0
- package/dist/themes/data/gruvbox.js +177 -0
- package/dist/themes/data/index.d.ts +35 -0
- package/dist/themes/data/index.js +64 -0
- package/dist/themes/data/monokai.d.ts +3 -0
- package/dist/themes/data/monokai.js +177 -0
- package/dist/themes/data/nord.d.ts +3 -0
- package/dist/themes/data/nord.js +177 -0
- package/dist/themes/data/rose-pine-dawn.d.ts +3 -0
- package/dist/themes/data/rose-pine-dawn.js +177 -0
- package/dist/themes/data/rose-pine-moon.d.ts +3 -0
- package/dist/themes/data/rose-pine-moon.js +177 -0
- package/dist/themes/data/rose-pine.d.ts +3 -0
- package/dist/themes/data/rose-pine.js +177 -0
- package/dist/themes/data/solarized-dark.d.ts +3 -0
- package/dist/themes/data/solarized-dark.js +177 -0
- package/dist/themes/data/solarized-light.d.ts +3 -0
- package/dist/themes/data/solarized-light.js +177 -0
- package/dist/themes/data/svg-export.d.ts +3 -0
- package/dist/themes/data/svg-export.js +177 -0
- package/dist/themes/data/textual-ansi.d.ts +3 -0
- package/dist/themes/data/textual-ansi.js +160 -0
- package/dist/themes/data/textual-dark.d.ts +3 -0
- package/dist/themes/data/textual-dark.js +160 -0
- package/dist/themes/data/textual-light.d.ts +3 -0
- package/dist/themes/data/textual-light.js +160 -0
- package/dist/themes/data/tokyo-night.d.ts +3 -0
- package/dist/themes/data/tokyo-night.js +177 -0
- package/dist/themes/data/types.d.ts +31 -0
- package/dist/themes/data/types.js +10 -0
- package/dist/themes/palette.d.ts +45 -0
- package/dist/themes/palette.js +71 -0
- package/dist/themes/ramp.d.ts +70 -0
- package/dist/themes/ramp.js +103 -0
- package/dist/themes/registry.d.ts +44 -0
- package/dist/themes/registry.js +105 -0
- package/dist/themes/terminalThemes.d.ts +49 -0
- package/dist/themes/terminalThemes.js +63 -0
- package/dist/themes/transpose.d.ts +87 -0
- package/dist/themes/transpose.js +138 -0
- package/dist/widgets/button.d.ts +45 -0
- package/dist/widgets/button.js +177 -0
- package/dist/widgets/checkbox.d.ts +38 -0
- package/dist/widgets/checkbox.js +135 -0
- package/dist/widgets/dropdown.d.ts +64 -0
- package/dist/widgets/dropdown.js +416 -0
- package/dist/widgets/event-router.d.ts +71 -0
- package/dist/widgets/event-router.js +629 -0
- package/dist/widgets/focus-manager.d.ts +45 -0
- package/dist/widgets/focus-manager.js +191 -0
- package/dist/widgets/hit.d.ts +64 -0
- package/dist/widgets/hit.js +113 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +14 -0
- package/dist/widgets/ink.d.ts +22 -0
- package/dist/widgets/ink.js +31 -0
- package/dist/widgets/slider.d.ts +54 -0
- package/dist/widgets/slider.js +247 -0
- package/dist/widgets/static-item.d.ts +34 -0
- package/dist/widgets/static-item.js +54 -0
- package/dist/widgets/text-input.d.ts +283 -0
- package/dist/widgets/text-input.js +1174 -0
- package/dist/widgets/themed-widget.d.ts +20 -0
- package/dist/widgets/themed-widget.js +89 -0
- package/dist/widgets/toggle.d.ts +41 -0
- package/dist/widgets/toggle.js +157 -0
- package/dist/widgets/types.d.ts +87 -0
- package/dist/widgets/types.js +30 -0
- package/dist/widgets/widget-app.d.ts +65 -0
- package/dist/widgets/widget-app.js +206 -0
- package/dist/widgets/widget-base.d.ts +42 -0
- package/dist/widgets/widget-base.js +172 -0
- package/package.json +16 -2
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal cell width calculation.
|
|
3
|
+
* Handles ASCII, CJK (double-width), and emoji characters.
|
|
4
|
+
*/
|
|
5
|
+
import stringWidth from "string-width";
|
|
6
|
+
import { Memo } from "./memo.js";
|
|
7
|
+
// [LAW:one-source-of-truth] string-width is the single authority for cell width
|
|
8
|
+
const cellLenMemo = new Memo();
|
|
9
|
+
/**
|
|
10
|
+
* Returns the terminal cell width of a string.
|
|
11
|
+
*/
|
|
12
|
+
export function cellLen(text) {
|
|
13
|
+
if (text.length === 0)
|
|
14
|
+
return 0;
|
|
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);
|
|
17
|
+
}
|
|
18
|
+
/** Brand a raw number as a CellCol. Use only at trust boundaries. */
|
|
19
|
+
export function asCellCol(n) { return n; }
|
|
20
|
+
/** Brand a raw number as a CodeUnit. Use only at trust boundaries. */
|
|
21
|
+
export function asCodeUnit(n) { return n; }
|
|
22
|
+
/** Brand a raw number as a CodePoint. Use only when the value is known to be
|
|
23
|
+
* on a Unicode code-point boundary. */
|
|
24
|
+
export function asCodePoint(n) { return n; }
|
|
25
|
+
/**
|
|
26
|
+
* [LAW:parse-dont-validate] A caller's number as a count of cells — the checked
|
|
27
|
+
* counterpart to `asCellCol`'s unchecked brand, and the one place the rule "a
|
|
28
|
+
* cell count is a non-negative integer" is written.
|
|
29
|
+
*
|
|
30
|
+
* [LAW:one-source-of-truth] Every renderable is handed `RenderOptions.maxWidth`
|
|
31
|
+
* as a plain `number`, so every renderable used to answer this for itself and
|
|
32
|
+
* they disagreed: at NaN, `Table` collapsed to one cell of garbage, `Columns`
|
|
33
|
+
* and `Layout` threw `Invalid array length`, and `RichText` ignored the request
|
|
34
|
+
* and emitted its full natural width. Parse at the entry to `render`/`measure`
|
|
35
|
+
* and nothing downstream re-asks.
|
|
36
|
+
*
|
|
37
|
+
* The comparison is the point, not `Math.max(0, n)`: NaN fails every comparison
|
|
38
|
+
* and so floors here, where `Math.max` would return it and let it poison every
|
|
39
|
+
* bounds check downstream. Integral because a cell is not divisible — a
|
|
40
|
+
* fractional budget reaches a largest-remainder division, which grants a whole
|
|
41
|
+
* cell against a fractional residue and hands out more width than it was given.
|
|
42
|
+
*
|
|
43
|
+
* `Infinity` is outside what the comparison floors, so it passes through
|
|
44
|
+
* unchanged, and no clamp belongs here: the finite answer to an unbounded offer
|
|
45
|
+
* is the renderable's own natural width, which this function cannot see from
|
|
46
|
+
* the number alone. `withBoundedWidth` in `protocol.ts` is the second half of
|
|
47
|
+
* the parse and the only place that value is resolved.
|
|
48
|
+
*/
|
|
49
|
+
export function cellCount(n) {
|
|
50
|
+
return (n > 0 ? Math.floor(n) : 0);
|
|
51
|
+
}
|
|
52
|
+
// ── Cell-aware string utilities ──────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Pads or crops a string to exactly `totalWidth` terminal cells.
|
|
55
|
+
* Invariant: cellLen(setCellSize(text, n)) === n (unless n is 0)
|
|
56
|
+
*/
|
|
57
|
+
export function setCellSize(text, totalWidth) {
|
|
58
|
+
if (totalWidth === 0)
|
|
59
|
+
return "";
|
|
60
|
+
const currentWidth = cellLen(text);
|
|
61
|
+
if (currentWidth === totalWidth)
|
|
62
|
+
return text;
|
|
63
|
+
if (currentWidth < totalWidth) {
|
|
64
|
+
return text + " ".repeat(totalWidth - currentWidth);
|
|
65
|
+
}
|
|
66
|
+
return splitText(text, totalWidth)[0];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Splits text at a cell position. Returns [left, right].
|
|
70
|
+
* When the position falls mid-wide-character, the left side is padded
|
|
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.
|
|
75
|
+
*/
|
|
76
|
+
export function splitText(text, position) {
|
|
77
|
+
if (position <= 0)
|
|
78
|
+
return ["", text];
|
|
79
|
+
if (position >= cellLen(text))
|
|
80
|
+
return [text, ""];
|
|
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
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Wraps text into lines of at most `maxWidth` cells, preserving every code
|
|
104
|
+
* point: `chopCells(t, w).join("") === t`. Unlike `splitText` this never pads,
|
|
105
|
+
* so a line ending before a wide glyph is narrower than `maxWidth` rather than
|
|
106
|
+
* padded out to it.
|
|
107
|
+
*
|
|
108
|
+
* A line overflows exactly where the budget cannot be met: a glyph wider than
|
|
109
|
+
* `maxWidth` is force-taken whole rather than dropped, and a `maxWidth` of zero
|
|
110
|
+
* or less is no budget at all, so the text comes back as one line.
|
|
111
|
+
*/
|
|
112
|
+
export function chopCells(text, maxWidth) {
|
|
113
|
+
if (maxWidth <= 0 || text.length === 0)
|
|
114
|
+
return [text];
|
|
115
|
+
const lines = [];
|
|
116
|
+
let start = asCodePoint(0);
|
|
117
|
+
while (start < text.length) {
|
|
118
|
+
const end = cellStepFrom(text, start, maxWidth);
|
|
119
|
+
lines.push(text.slice(start, end));
|
|
120
|
+
start = end;
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
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.
|
|
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
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The grapheme clusters of `text`, in order: the unit `string-width` measures,
|
|
159
|
+
* and so the unit `cellLen` of a whole string is the sum of. Summing `cellLen`
|
|
160
|
+
* over code points instead disagrees with it on every glyph built from several:
|
|
161
|
+
* `❤️` is 1 + 0 by code point and 2 as a cluster, `👨👩👧` is 6 and 2, and a cut
|
|
162
|
+
* between two of its code points leaves half a glyph.
|
|
163
|
+
*/
|
|
164
|
+
export function graphemes(text) {
|
|
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
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Returns the largest prefix of `text` whose cell width fits within `cap` cells,
|
|
181
|
+
* cut between grapheme clusters. No padding — the returned string may be
|
|
182
|
+
* narrower than `cap` when the next glyph is wide and would overshoot. Never
|
|
183
|
+
* wider than `cap` cells.
|
|
184
|
+
*
|
|
185
|
+
* When the first glyph already exceeds `cap` cells, returns "" (the caller
|
|
186
|
+
* must decide whether to force-take the glyph or skip it).
|
|
187
|
+
*/
|
|
188
|
+
export function cellFit(text, cap) {
|
|
189
|
+
return text.slice(0, fitLength(clustersFrom(text, asCodePoint(0)), cap));
|
|
190
|
+
}
|
|
191
|
+
/** `cellFit` from the other end: the largest suffix of `text` within `cap` cells. */
|
|
192
|
+
export function cellFitEnd(text, cap) {
|
|
193
|
+
return text.slice(text.length - fitLength(graphemes(text).reverse(), cap));
|
|
194
|
+
}
|
|
195
|
+
/** The code units of the leading `clusters` that fit within `cap` cells. */
|
|
196
|
+
function fitLength(clusters, cap) {
|
|
197
|
+
let w = 0;
|
|
198
|
+
let n = 0;
|
|
199
|
+
for (const cluster of clusters) {
|
|
200
|
+
const cw = cellLen(cluster);
|
|
201
|
+
if (w + cw > cap)
|
|
202
|
+
break;
|
|
203
|
+
w += cw;
|
|
204
|
+
n += cluster.length;
|
|
205
|
+
}
|
|
206
|
+
return n;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Returns the largest code-unit end offset starting from `startCU` whose
|
|
210
|
+
* prefix (from `startCU`) has cell width ≤ `cap`. Iterates from the given
|
|
211
|
+
* offset without slicing the tail, avoiding O(N²) allocation when called
|
|
212
|
+
* repeatedly across a long string.
|
|
213
|
+
*
|
|
214
|
+
* [LAW:types-are-the-program] returns CodePoint because it stops between
|
|
215
|
+
* grapheme clusters, and every cluster boundary is a code-point boundary.
|
|
216
|
+
*/
|
|
217
|
+
export function cellFitFrom(text, startCU, cap) {
|
|
218
|
+
return asCodePoint(startCU + fitLength(clustersFrom(text, startCU), cap));
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Like `cellFitFrom`, but for any `startCU` inside `text` the result is
|
|
222
|
+
* strictly past it: a glyph too wide for `cap` is force-taken whole and
|
|
223
|
+
* overflows `cap` by its own width. So a `while (i < text.length)` loop driven
|
|
224
|
+
* by this terminates by construction, and the re-brand is honest — both
|
|
225
|
+
* operands land on code-point boundaries, so their max does too.
|
|
226
|
+
*
|
|
227
|
+
* [LAW:single-enforcer] `cellFit` documents that its caller must choose between
|
|
228
|
+
* force-taking and skipping; this is that choice, made once. It was made twice
|
|
229
|
+
* before and the copies drifted — the textarea's soft wrap force-took,
|
|
230
|
+
* `chopCells` did neither and hung.
|
|
231
|
+
*/
|
|
232
|
+
export function cellStepFrom(text, startCU, cap) {
|
|
233
|
+
const [first = ""] = clustersFrom(text, startCU);
|
|
234
|
+
return asCodePoint(Math.max(cellFitFrom(text, startCU, cap), startCU + first.length));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
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))`.
|
|
240
|
+
*/
|
|
241
|
+
export function cellColToCodeUnitOffset(content, cellCol) {
|
|
242
|
+
return cellFitFrom(content, asCodePoint(0), cellCol);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
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`.
|
|
249
|
+
*/
|
|
250
|
+
export function nextGrapheme(s, cu) {
|
|
251
|
+
const [cluster = ""] = clustersFrom(s, cu);
|
|
252
|
+
return asCodePoint(cu + cluster.length);
|
|
253
|
+
}
|
|
254
|
+
/** `nextGrapheme` backwards: the start of the cluster ending at `cu`; 0 at the start. */
|
|
255
|
+
export function prevGrapheme(s, cu) {
|
|
256
|
+
if (cu <= 0)
|
|
257
|
+
return asCodePoint(0);
|
|
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}`);
|
|
279
|
+
}
|
|
280
|
+
return n;
|
|
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;
|
|
318
|
+
}
|
|
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 };
|
|
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 {};
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
// The longest delay a platform timer holds, in seconds: setInterval clamps a
|
|
19
|
+
// longer one to a millisecond, which would turn the slowest rate into the
|
|
20
|
+
// fastest.
|
|
21
|
+
const LONGEST_INTERVAL = (2 ** 31 - 1) / 1000;
|
|
22
|
+
/**
|
|
23
|
+
* `perSecond` as a `FrameRate`; anything but a positive, finite number throws,
|
|
24
|
+
* and so does a rate whose frames are further apart than a timer can wait —
|
|
25
|
+
* about 24.8 days.
|
|
26
|
+
*/
|
|
27
|
+
export function frameRate(perSecond) {
|
|
28
|
+
if (!(Number.isFinite(perSecond) && perSecond > 0 && 1 / perSecond <= LONGEST_INTERVAL)) {
|
|
29
|
+
throw new RangeError(`a frame rate is a positive, finite number of frames a second, at most ${LONGEST_INTERVAL} s apart, not ${perSecond}`);
|
|
30
|
+
}
|
|
31
|
+
return { perSecond, interval: 1 / perSecond };
|
|
32
|
+
}
|
|
33
|
+
/** The platform's monotonic clock and its interval timer. */
|
|
34
|
+
export function systemClock() {
|
|
35
|
+
return {
|
|
36
|
+
now: () => performance.now() / 1000,
|
|
37
|
+
every: (rate, tick) => {
|
|
38
|
+
const timer = setInterval(tick, rate.interval * 1000);
|
|
39
|
+
return () => clearInterval(timer);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|