@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/dist/core/text.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RichText — styled text with spans. The primary text type for the library.
|
|
3
3
|
*/
|
|
4
|
-
import { cellLen, cellCount, cellFit, cellFitEnd, asCellCol } from "./cells.js";
|
|
5
|
-
import { divideLine } from "./wrap.js";
|
|
4
|
+
import { cellLen, cellCount, cellFit, cellFitEnd, asCellCol, expandTabs, parseTabSize } from "./cells.js";
|
|
5
|
+
import { divideLine, wordWidths } from "./wrap.js";
|
|
6
6
|
import { Segment } from "./segment.js";
|
|
7
7
|
import { Style, NULL_STYLE, StyleSyntaxError } from "./style.js";
|
|
8
8
|
import { stripOscTerminators } from "./osc8.js";
|
|
9
9
|
import { drawable, getStyle, withBoundedWidth } from "./protocol.js";
|
|
10
|
-
//
|
|
10
|
+
// Every C0 control and DEL except \t and \n. Rich's `Text` strips only
|
|
11
|
+
// `STRIP_CONTROL_CODES` (BEL, BS, VT, FF, CR); the rest of the set is the
|
|
12
|
+
// port's extension, so no C0 byte of content can start an escape sequence or
|
|
13
|
+
// move the cursor on the wire. C1 controls (U+0080–U+009F) pass, as in Rich.
|
|
11
14
|
// [LAW:single-enforcer] Single place where control chars are sanitized
|
|
12
|
-
const CONTROL_CHARS_RE = /[\x00-\x08\x0B
|
|
15
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0B-\x1F\x7F]/g;
|
|
13
16
|
export function stripControlChars(text) {
|
|
14
17
|
return text.replace(CONTROL_CHARS_RE, "");
|
|
15
18
|
}
|
|
@@ -64,8 +67,8 @@ function sanitizeStyleLink(style) {
|
|
|
64
67
|
* A style as a RichText keeps it: as given. A name stays a name because what
|
|
65
68
|
* it stands for depends on the theme of the render that draws it, which no
|
|
66
69
|
* RichText knows when the name arrives — the reference stores span styles the
|
|
67
|
-
* same way. A string holds no link until it is parsed, so only a `Style`
|
|
68
|
-
* one to sanitize here.
|
|
70
|
+
* same way. A string holds no link until it is parsed, so only a `Style` —
|
|
71
|
+
* alone or laid over a name — has one to sanitize here.
|
|
69
72
|
*
|
|
70
73
|
* [LAW:single-enforcer] Nor does a RichText keep an anchor (`./anchor.ts`). It
|
|
71
74
|
* lays its text out anew, so a stamp naming where a cell sat in the output it
|
|
@@ -73,8 +76,13 @@ function sanitizeStyleLink(style) {
|
|
|
73
76
|
* owner that renders the RichText is the one that stamps its cells.
|
|
74
77
|
*/
|
|
75
78
|
function admitStyle(style) {
|
|
76
|
-
if (
|
|
79
|
+
if (typeof style === "string")
|
|
77
80
|
return style;
|
|
81
|
+
if (style instanceof Style)
|
|
82
|
+
return admitDefined(style);
|
|
83
|
+
return { under: style.under, over: admitDefined(style.over) };
|
|
84
|
+
}
|
|
85
|
+
function admitDefined(style) {
|
|
78
86
|
return sanitizeStyleLink(style.anchor ? style.withAnchor(undefined) : style);
|
|
79
87
|
}
|
|
80
88
|
/** A style that adds nothing: the empty definition, or a null `Style`. */
|
|
@@ -82,13 +90,16 @@ function isEmptyStyle(style) {
|
|
|
82
90
|
return style instanceof Style ? style.isNull : style === "";
|
|
83
91
|
}
|
|
84
92
|
/**
|
|
85
|
-
* The style a stored `
|
|
93
|
+
* The style a stored `TextStyle` stands for in this render.
|
|
86
94
|
*
|
|
87
95
|
* A parsed string can carry a link, so the result is sanitized on the way out
|
|
88
96
|
* as well.
|
|
89
97
|
*/
|
|
90
|
-
function resolveStyle(options, style) {
|
|
91
|
-
|
|
98
|
+
export function resolveStyle(options, style) {
|
|
99
|
+
const resolved = style instanceof Style ? style
|
|
100
|
+
: typeof style === "string" ? resolveDefinition(options, style)
|
|
101
|
+
: resolveDefinition(options, style.under).add(style.over);
|
|
102
|
+
return sanitizeStyleLink(resolved);
|
|
92
103
|
}
|
|
93
104
|
/**
|
|
94
105
|
* [LAW:single-enforcer] Styling is non-critical — an unrecognized style name
|
|
@@ -163,7 +174,7 @@ export class RichText {
|
|
|
163
174
|
this._justify = options?.justify;
|
|
164
175
|
this._overflow = options?.overflow;
|
|
165
176
|
this._end = options?.end ?? "\n";
|
|
166
|
-
this._tabSize = options?.tabSize ?? 8;
|
|
177
|
+
this._tabSize = parseTabSize(options?.tabSize ?? 8);
|
|
167
178
|
this._noWrap = options?.noWrap ?? false;
|
|
168
179
|
}
|
|
169
180
|
// --- Properties ---
|
|
@@ -182,13 +193,21 @@ export class RichText {
|
|
|
182
193
|
get length() {
|
|
183
194
|
return this._text.length;
|
|
184
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* The cells of the plain text with each tab widened to its stop counted from
|
|
198
|
+
* this text's own column 0, as `render` widens it — so it is the drawn width
|
|
199
|
+
* of a one-line text set at a stop. A deliberate departure from Rich, whose
|
|
200
|
+
* `Text.cell_len` counts a tab as no cells, so everything sized by it — a
|
|
201
|
+
* truncation, an alignment, a header's remaining width — decided on a width
|
|
202
|
+
* no render draws.
|
|
203
|
+
*/
|
|
185
204
|
get cellLength() {
|
|
186
|
-
return cellLen(this._text);
|
|
205
|
+
return cellLen(expandTabs(this._text, this._tabSize).text);
|
|
187
206
|
}
|
|
188
207
|
get hasContent() {
|
|
189
208
|
return this._text.length > 0;
|
|
190
209
|
}
|
|
191
|
-
/** The base style every span layers over
|
|
210
|
+
/** The base style every span layers over, resolved at render unless it is a `Style`. */
|
|
192
211
|
get style() {
|
|
193
212
|
return this._style;
|
|
194
213
|
}
|
|
@@ -458,6 +477,17 @@ export class RichText {
|
|
|
458
477
|
this._text = padding + this._text + padding;
|
|
459
478
|
return this;
|
|
460
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Each tab widened to the next tab stop, in place, as Rich's
|
|
482
|
+
* `Text.expand_tabs` widens it: spans move with the characters they style, so
|
|
483
|
+
* the text measures what it will draw.
|
|
484
|
+
*/
|
|
485
|
+
expandTabs() {
|
|
486
|
+
const expanded = expandTabs(this._text, this._tabSize);
|
|
487
|
+
this._spans = this._spans.map((s) => new Span(expanded.offsetOf(s.start), expanded.offsetOf(s.end), s.style));
|
|
488
|
+
this._text = expanded.text;
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
461
491
|
padLeft(count, char) {
|
|
462
492
|
const c = char ?? " ";
|
|
463
493
|
this._spans = this._spans.map((s) => s.move(count));
|
|
@@ -506,6 +536,10 @@ export class RichText {
|
|
|
506
536
|
* styling; the marker (if any) is inserted as plain text with no span.
|
|
507
537
|
* Use `stylize(...)` on the result to color the marker if needed.
|
|
508
538
|
*
|
|
539
|
+
* Text that fits comes back untouched. Text that is cut has its tabs widened
|
|
540
|
+
* to their stops first, as `expandTabs` widens them, so the width cut to is
|
|
541
|
+
* the width drawn.
|
|
542
|
+
*
|
|
509
543
|
* [LAW:dataflow-not-control-flow] mode/marker/width all flow as values;
|
|
510
544
|
* the walk is the same shape regardless. No "if truncated then rebuild"
|
|
511
545
|
* branch \u2014 the unchanged path just early-returns when content fits.
|
|
@@ -513,6 +547,9 @@ export class RichText {
|
|
|
513
547
|
truncate(width, options) {
|
|
514
548
|
if (this.cellLength <= width)
|
|
515
549
|
return this;
|
|
550
|
+
// [LAW:one-source-of-truth] Cut what is drawn: once a tab is spaces, a cell
|
|
551
|
+
// of the plain text is a cell of the output, so the cut lands where it looks.
|
|
552
|
+
this.expandTabs();
|
|
516
553
|
const mode = options?.mode ?? "right";
|
|
517
554
|
// [LAW:dataflow-not-control-flow] the marker is fitted to the width as a
|
|
518
555
|
// value, so width 0 and a marker wider than the width run the same walk
|
|
@@ -592,6 +629,9 @@ export class RichText {
|
|
|
592
629
|
const currentWidth = this.cellLength;
|
|
593
630
|
if (currentWidth >= width)
|
|
594
631
|
return this;
|
|
632
|
+
// Padded to `width`, the text claims that width only while its tab stops
|
|
633
|
+
// hold: widened now, padding on either side cannot move them.
|
|
634
|
+
this.expandTabs();
|
|
595
635
|
const gap = width - currentWidth;
|
|
596
636
|
switch (justify) {
|
|
597
637
|
case "left":
|
|
@@ -670,9 +710,17 @@ export class RichText {
|
|
|
670
710
|
}
|
|
671
711
|
// --- Renderable ---
|
|
672
712
|
*render(options) {
|
|
673
|
-
const
|
|
713
|
+
const expanded = expandTabs(this._text, this._tabSize);
|
|
674
714
|
const base = this.resolvedStyle(options);
|
|
675
|
-
const allSegments = this._buildSegments(
|
|
715
|
+
const { segments: allSegments, covered } = this._buildSegments(expanded, base, options);
|
|
716
|
+
const spanFree = spanFreeLines(allSegments, covered);
|
|
717
|
+
// Rich wraps the lines of `split(allow_blank=True)`: n line breaks make
|
|
718
|
+
// n + 1 lines, the last one blank when the text ends in a break, and that
|
|
719
|
+
// blank line is justified like any other. `Segment.splitLines` is Rich's
|
|
720
|
+
// `Segment.split_lines`, which drops a trailing empty line, so the text is
|
|
721
|
+
// handed to it closed by one more break — the dropped line is then always
|
|
722
|
+
// that one, and every line the text has comes back.
|
|
723
|
+
allSegments.push(Segment.line());
|
|
676
724
|
const logicalLines = Segment.splitLines(allSegments);
|
|
677
725
|
// [LAW:single-enforcer] The one crossing for this renderable's width, and
|
|
678
726
|
// the call every other renderable already makes. A bare `cellCount` stood
|
|
@@ -703,16 +751,19 @@ export class RichText {
|
|
|
703
751
|
const justify = overflow === "ignore" ? undefined : this._justify ?? options.justify;
|
|
704
752
|
// One cell either way, so the marker's stand-in leaves the cut where it was.
|
|
705
753
|
const ellipsis = drawable(options, "\u2026", ".");
|
|
706
|
-
const endsWithNewline = text.endsWith("\n");
|
|
707
754
|
for (let index = 0; index < logicalLines.length; index += 1) {
|
|
708
755
|
const line = logicalLines[index];
|
|
709
|
-
const
|
|
756
|
+
const free = spanFree[index];
|
|
757
|
+
const terminateLine = index < logicalLines.length - 1;
|
|
710
758
|
// Wrap first, overflow last — the reference's order, and the reason a
|
|
711
759
|
// long sentence grows a table row while an unbreakable word in the same
|
|
712
760
|
// column still ellipsizes.
|
|
713
761
|
const cuts = divideLine(plainOf(line), wrapAt, { fold: overflow === "fold" });
|
|
714
762
|
const wrapped = Segment.divide(line, cuts);
|
|
715
|
-
const placed = this._justifyLines(wrapped.map((piece) =>
|
|
763
|
+
const placed = this._justifyLines(wrapped.map((piece, at) => ({
|
|
764
|
+
segments: [...this._fitLine(piece, edge, overflow, ellipsis)],
|
|
765
|
+
free: (cell) => free(at === 0 ? cell : cuts[at - 1] + cell),
|
|
766
|
+
})), maxWidth, base, justify);
|
|
716
767
|
for (let piece = 0; piece < placed.length; piece += 1) {
|
|
717
768
|
yield* placed[piece];
|
|
718
769
|
if (piece < placed.length - 1 || terminateLine) {
|
|
@@ -724,29 +775,23 @@ export class RichText {
|
|
|
724
775
|
// non-empty text alike — the pre-fix code checked it once for empty text
|
|
725
776
|
// (truthy) and again for non-empty text (truthy and not the default
|
|
726
777
|
// "\n"), which gave `end` two meanings depending on whether the text was
|
|
727
|
-
// empty (rich-text-5ai).
|
|
728
|
-
// above, so this is reached either way with nothing yielded but this.
|
|
778
|
+
// empty (rich-text-5ai).
|
|
729
779
|
if (this._end) {
|
|
730
780
|
yield new Segment(this._end);
|
|
731
781
|
}
|
|
732
782
|
}
|
|
733
783
|
// --- Measurable ---
|
|
734
784
|
measure(options) {
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (word.length > 0) {
|
|
746
|
-
maxWordWidth = Math.max(maxWordWidth, cellLen(word));
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
785
|
+
const lines = expandTabs(this._text, this._tabSize).text.split("\n");
|
|
786
|
+
const maxLineWidth = lines.reduce((widest, line) => Math.max(widest, cellLen(line)), 0);
|
|
787
|
+
// [LAW:one-source-of-truth] The minimum is the widest piece the wrapper
|
|
788
|
+
// never breaks, asked of the wrapper — so a line's indent, which wraps
|
|
789
|
+
// with its first word, counts toward it. Rich splits on whitespace here
|
|
790
|
+
// and drops the indent, then wraps with it kept: a container sized by
|
|
791
|
+
// that minimum cuts the word it was meant to hold whole. Text with no word
|
|
792
|
+
// at all is all whitespace, and as in Rich its whole width is the minimum.
|
|
793
|
+
const widths = lines.flatMap(wordWidths);
|
|
794
|
+
const maxWordWidth = widths.length > 0 ? widths.reduce((widest, w) => Math.max(widest, w), 0) : maxLineWidth;
|
|
750
795
|
// Parsed for the same reason `render` parses it: an unparsed NaN ceiling
|
|
751
796
|
// makes both `Math.min` calls NaN, and a range of NaN..NaN is one no parent
|
|
752
797
|
// layout can divide.
|
|
@@ -757,11 +802,6 @@ export class RichText {
|
|
|
757
802
|
};
|
|
758
803
|
}
|
|
759
804
|
// --- Internal ---
|
|
760
|
-
_expandTabs(text) {
|
|
761
|
-
if (!text.includes("\t"))
|
|
762
|
-
return text;
|
|
763
|
-
return text.replace(/\t/g, " ".repeat(this._tabSize));
|
|
764
|
-
}
|
|
765
805
|
/**
|
|
766
806
|
* The rendered text cut at every span edge, each piece carrying the base
|
|
767
807
|
* style plus every span covering it.
|
|
@@ -787,27 +827,41 @@ export class RichText {
|
|
|
787
827
|
* always did, and then folds into no piece at all: its range comes out empty
|
|
788
828
|
* and no case handles it.
|
|
789
829
|
*/
|
|
790
|
-
_buildSegments(text, base, options) {
|
|
791
|
-
|
|
792
|
-
|
|
830
|
+
_buildSegments({ text, offsetOf }, base, options) {
|
|
831
|
+
// A span is written against the text as given; `offsetOf` moves its edges
|
|
832
|
+
// past every tab that widened before them.
|
|
833
|
+
const clamp = (offset) => Math.max(0, Math.min(offsetOf(offset), text.length));
|
|
834
|
+
// The text's own start and end are always the outer boundaries, even when
|
|
835
|
+
// they are one offset: empty text is one empty piece, as Rich's
|
|
836
|
+
// `Text.render` yields `Segment("")` for it. That piece writes no bytes,
|
|
837
|
+
// and it is what makes empty text one line to everything that splits a
|
|
838
|
+
// render into lines — a panel's body, a table cell — where Rich draws one.
|
|
839
|
+
const inner = new Set();
|
|
793
840
|
for (const span of this._spans) {
|
|
794
|
-
|
|
795
|
-
|
|
841
|
+
inner.add(clamp(span.start));
|
|
842
|
+
inner.add(clamp(span.end));
|
|
796
843
|
}
|
|
797
|
-
|
|
844
|
+
inner.delete(0);
|
|
845
|
+
inner.delete(text.length);
|
|
846
|
+
const boundaries = [0, ...[...inner].sort((a, b) => a - b), text.length];
|
|
798
847
|
// Every span edge is a boundary, so where a span's range opens is a lookup
|
|
799
|
-
// rather than a search.
|
|
800
|
-
|
|
848
|
+
// rather than a search. Where two boundaries share an offset — empty
|
|
849
|
+
// text's start and end — the first wins, so the offset names the piece
|
|
850
|
+
// that opens there rather than the one past the last.
|
|
851
|
+
const pieceAt = new Map(boundaries.map((position, piece) => [position, piece]).reverse());
|
|
801
852
|
const styles = boundaries.slice(0, -1).map(() => base);
|
|
853
|
+
const covered = styles.map(() => false);
|
|
802
854
|
for (const span of this._spans) {
|
|
803
855
|
const end = clamp(span.end);
|
|
804
856
|
const style = resolveStyle(options, span.style);
|
|
805
857
|
const opensAt = pieceAt.get(clamp(span.start));
|
|
806
858
|
for (let piece = opensAt; boundaries[piece] < end; piece++) {
|
|
807
859
|
styles[piece] = styles[piece].add(style);
|
|
860
|
+
covered[piece] = true;
|
|
808
861
|
}
|
|
809
862
|
}
|
|
810
|
-
|
|
863
|
+
const segments = styles.map((style, piece) => new Segment(text.slice(boundaries[piece], boundaries[piece + 1]), style.isNull ? undefined : style));
|
|
864
|
+
return { segments, covered };
|
|
811
865
|
}
|
|
812
866
|
/**
|
|
813
867
|
* The pieces one logical line wrapped into, each placed in a canvas
|
|
@@ -823,9 +877,9 @@ export class RichText {
|
|
|
823
877
|
*/
|
|
824
878
|
_justifyLines(lines, maxWidth, base, justify) {
|
|
825
879
|
if (justify !== "full") {
|
|
826
|
-
return lines.map((line) => [...this._justifyLine(line, maxWidth, justify)]);
|
|
880
|
+
return lines.map((line) => [...this._justifyLine(line, maxWidth, base, justify)]);
|
|
827
881
|
}
|
|
828
|
-
return lines.map((
|
|
882
|
+
return lines.map(({ segments }, index) => index === lines.length - 1 ? segments : this._fillLine(segments, maxWidth, base));
|
|
829
883
|
}
|
|
830
884
|
/**
|
|
831
885
|
* One line placed in a canvas `maxWidth` wide.
|
|
@@ -840,29 +894,28 @@ export class RichText {
|
|
|
840
894
|
* `undefined` is not `"left"`: it is Rich's `"default"`, which places the
|
|
841
895
|
* line without padding it at all. That distinction is what lets a soft-wrapped
|
|
842
896
|
* `Console.print` leave its lines at their natural width.
|
|
897
|
+
*
|
|
898
|
+
* The padding is drawn in the text's base style, as the reference pads
|
|
899
|
+
* inside the `Text` it justifies: a title "on red" fills its row in red.
|
|
843
900
|
*/
|
|
844
|
-
*_justifyLine(line, maxWidth,
|
|
901
|
+
*_justifyLine({ segments: line, free }, maxWidth, base,
|
|
845
902
|
// [LAW:types-are-the-program] `full` is absent rather than ignored: it
|
|
846
903
|
// needs the lines either side of this one, so the type refuses it here
|
|
847
904
|
// instead of a branch quietly rendering it as `left`, which is the bug
|
|
848
905
|
// this signature replaces (rich-justify-0cr.1).
|
|
849
906
|
justify) {
|
|
907
|
+
const padStyle = base.isNull ? undefined : base;
|
|
850
908
|
switch (justify) {
|
|
851
909
|
case "center":
|
|
852
910
|
case "right": {
|
|
853
911
|
const body = Segment.adjustLineLength(line, Segment.getLineLength(line) - hangingWhitespace(line), undefined, false);
|
|
854
912
|
const gap = Math.max(maxWidth - Segment.getLineLength(body), 0);
|
|
855
913
|
const leftPad = justify === "center" ? Math.floor(gap / 2) : gap;
|
|
856
|
-
|
|
857
|
-
yield new Segment(" ".repeat(leftPad));
|
|
858
|
-
yield* body;
|
|
859
|
-
const rightPad = gap - leftPad;
|
|
860
|
-
if (rightPad > 0)
|
|
861
|
-
yield new Segment(" ".repeat(rightPad));
|
|
914
|
+
yield* padded(body, free, leftPad, gap - leftPad, padStyle);
|
|
862
915
|
break;
|
|
863
916
|
}
|
|
864
917
|
case "left":
|
|
865
|
-
yield*
|
|
918
|
+
yield* padded(line, free, 0, Math.max(maxWidth - Segment.getLineLength(line), 0), padStyle);
|
|
866
919
|
break;
|
|
867
920
|
default:
|
|
868
921
|
yield* line;
|
|
@@ -974,3 +1027,71 @@ export class RichText {
|
|
|
974
1027
|
yield* Segment.adjustLineLength(line, maxWidth, undefined, false);
|
|
975
1028
|
}
|
|
976
1029
|
}
|
|
1030
|
+
/**
|
|
1031
|
+
* For each logical line of a render, whether the cell at an offset is covered
|
|
1032
|
+
* by no span. `covered` is `_buildSegments`'s answer per piece; a piece split
|
|
1033
|
+
* by a line break answers for both halves.
|
|
1034
|
+
*/
|
|
1035
|
+
function spanFreeLines(segments, covered) {
|
|
1036
|
+
// Per line, the cell each run ends at and whether that run is span-free.
|
|
1037
|
+
const lines = [[]];
|
|
1038
|
+
segments.forEach((segment, piece) => {
|
|
1039
|
+
segment.text.split("\n").forEach((part, at) => {
|
|
1040
|
+
if (at > 0)
|
|
1041
|
+
lines.push([]);
|
|
1042
|
+
const runs = lines.at(-1);
|
|
1043
|
+
runs.push({ end: (runs.at(-1)?.end ?? 0) + cellLen(part), free: !covered[piece] });
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
return lines.map((runs) => (cell) => {
|
|
1047
|
+
let lo = 0;
|
|
1048
|
+
let hi = runs.length;
|
|
1049
|
+
while (lo < hi) {
|
|
1050
|
+
const mid = (lo + hi) >> 1;
|
|
1051
|
+
if (runs[mid].end <= cell)
|
|
1052
|
+
lo = mid + 1;
|
|
1053
|
+
else
|
|
1054
|
+
hi = mid;
|
|
1055
|
+
}
|
|
1056
|
+
return runs[lo].free;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* A line with `left` and `right` cells of padding in `style` either side.
|
|
1061
|
+
*
|
|
1062
|
+
* The reference pads inside the `Text` it justifies and cuts its output at
|
|
1063
|
+
* span edges, so a pad and the run of the line beside it that no span covers
|
|
1064
|
+
* are one piece, and Rich emits them as one segment; a run a span covers stays
|
|
1065
|
+
* its own segment even when its style draws the same as the pad
|
|
1066
|
+
* (rich-table-qj6i). A span-free run is drawn in `style` alone, which is what
|
|
1067
|
+
* makes the joined segment's style exact.
|
|
1068
|
+
*/
|
|
1069
|
+
function* padded(drawn, free, left, right, style) {
|
|
1070
|
+
if (left + right === 0)
|
|
1071
|
+
return yield* drawn;
|
|
1072
|
+
// An empty piece writes no bytes, and at an edge it would take the join that
|
|
1073
|
+
// belongs to the run behind it.
|
|
1074
|
+
const line = drawn.filter((segment) => segment.text !== "");
|
|
1075
|
+
const pad = (cells) => " ".repeat(cells);
|
|
1076
|
+
const width = Segment.getLineLength(line);
|
|
1077
|
+
// A line with no cells has no run to join; the pads alone are its one piece.
|
|
1078
|
+
if (width === 0)
|
|
1079
|
+
return yield new Segment(pad(left + right), style);
|
|
1080
|
+
const last = line.length - 1;
|
|
1081
|
+
// A zero-width segment has no cell of its own to ask about, so it never
|
|
1082
|
+
// joins: merged on its neighbour's answer it would lose its own style.
|
|
1083
|
+
const head = left > 0 && line[0].cellLength > 0 && free(0);
|
|
1084
|
+
const tail = right > 0 && line[last].cellLength > 0 && free(width - 1);
|
|
1085
|
+
const from = head ? 1 : 0;
|
|
1086
|
+
const to = tail ? last : last + 1;
|
|
1087
|
+
// One run joined on both sides: the pads and it are one piece.
|
|
1088
|
+
if (from > to) {
|
|
1089
|
+
yield new Segment(pad(left) + line[0].text + pad(right), style);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (left > 0)
|
|
1093
|
+
yield new Segment(pad(left) + (head ? line[0].text : ""), style);
|
|
1094
|
+
yield* line.slice(from, to);
|
|
1095
|
+
if (right > 0)
|
|
1096
|
+
yield new Segment((tail ? line[last].text : "") + pad(right), style);
|
|
1097
|
+
}
|
package/dist/core/wrap.d.ts
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* same cell still ellipsizes.
|
|
21
21
|
*/
|
|
22
22
|
import { type CellCol } from "./cells.js";
|
|
23
|
+
/**
|
|
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.
|
|
26
|
+
*/
|
|
27
|
+
export declare function wordWidths(text: string): number[];
|
|
23
28
|
/**
|
|
24
29
|
* The cell offsets `text` should be cut at to wrap it into lines of at most
|
|
25
30
|
* `width` cells, for `Segment.divide` to apply to the styled line.
|
package/dist/core/wrap.js
CHANGED
|
@@ -39,6 +39,21 @@ function* words(text) {
|
|
|
39
39
|
yield match[0];
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The cells a word needs on a line: all of it but its trailing whitespace,
|
|
44
|
+
* which may hang past the edge for the caller to crop. Its leading whitespace
|
|
45
|
+
* counts — that is a line's indent, and it is not a place to break.
|
|
46
|
+
*/
|
|
47
|
+
function widthOf(word) {
|
|
48
|
+
return cellLen(word.trimEnd());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
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.
|
|
53
|
+
*/
|
|
54
|
+
export function wordWidths(text) {
|
|
55
|
+
return Array.from(words(text), widthOf);
|
|
56
|
+
}
|
|
42
57
|
/**
|
|
43
58
|
* A word chopped into pieces of at most `width` cells, as the reference chops
|
|
44
59
|
* it.
|
|
@@ -92,9 +107,7 @@ export function divideLine(text, width, options) {
|
|
|
92
107
|
let used = 0; // cells already committed to the line being filled
|
|
93
108
|
let wordStart = 0; // absolute cell offset where the current word begins
|
|
94
109
|
for (const word of words(text)) {
|
|
95
|
-
|
|
96
|
-
// line with its spaces hanging past the edge, and the caller crops them.
|
|
97
|
-
const wordWidth = cellLen(word.trimEnd());
|
|
110
|
+
const wordWidth = widthOf(word);
|
|
98
111
|
if (width - used >= wordWidth) {
|
|
99
112
|
used += cellLen(word);
|
|
100
113
|
}
|
package/dist/host/app.d.ts
CHANGED
|
@@ -23,56 +23,100 @@
|
|
|
23
23
|
* frame's encoding. What differs per host — whether the program can end
|
|
24
24
|
* under us, how it is suspended — is the `TerminalHost`'s, so the same app
|
|
25
25
|
* runs in node and in a browser with no path of its own for either.
|
|
26
|
+
*
|
|
27
|
+
* [LAW:single-enforcer] Input has one reader: the app subscribes to its
|
|
28
|
+
* host's bytes once and parses them (`host/input`), so a program hears keys
|
|
29
|
+
* and the pointer as events and never decodes a byte. The quit keys are
|
|
30
|
+
* checked here, before any handler hears the key, so nothing a program binds
|
|
31
|
+
* — a focused field in a `WidgetApp` included — can swallow them.
|
|
26
32
|
*/
|
|
33
|
+
import { type Clock, type FrameRate } from "../core/clock.js";
|
|
34
|
+
import { type ConsoleOptions } from "../core/console.js";
|
|
27
35
|
import { Segment } from "../core/segment.js";
|
|
36
|
+
import { type Surface } from "../core/paint.js";
|
|
28
37
|
import { type Renderable } from "../core/protocol.js";
|
|
29
38
|
import type { Unsubscribe } from "../core/subscription.js";
|
|
39
|
+
import { type KeyPress, type ScreenMouseEvent } from "./input.js";
|
|
30
40
|
import type { TerminalHost } from "./terminal-host.js";
|
|
31
|
-
/**
|
|
32
|
-
* Where the frame is painted. `alternate` is the whole terminal, in the
|
|
33
|
-
* alternate screen buffer, so the rows the program printed before it are
|
|
34
|
-
* there again when it stops; the terminal reports the pointer on it. `inline`
|
|
35
|
-
* starts at the cursor's line, as tall as the frame, and stays on the
|
|
36
|
-
* terminal when the app stops. It gets no pointer events: the terminal
|
|
37
|
-
* reports a pointer by its row on the screen, and the app does not know
|
|
38
|
-
* which row of the screen its frame starts on.
|
|
39
|
-
*/
|
|
40
|
-
export type Surface = "alternate" | "inline";
|
|
41
41
|
/**
|
|
42
42
|
* Where an app is in its life. `idle` has not run; `running` holds the
|
|
43
43
|
* terminal and paints; `suspended` has handed it back until the host resumes
|
|
44
44
|
* the program; `stopped` has handed it back for good.
|
|
45
45
|
*/
|
|
46
46
|
export type AppPhase = "idle" | "running" | "suspended" | "stopped";
|
|
47
|
-
|
|
47
|
+
declare const DRAW_OPTIONS: readonly ["asciiOnly", "theme", "onStyleError", "markup", "highlight", "highlighter"];
|
|
48
|
+
type DrawOption = (typeof DRAW_OPTIONS)[number];
|
|
49
|
+
type DrawOptions = Readonly<Pick<ConsoleOptions, DrawOption>>;
|
|
50
|
+
export interface AppOptions extends DrawOptions {
|
|
48
51
|
/** The terminal the app runs on. The app starts and stops it. */
|
|
49
52
|
readonly host: TerminalHost;
|
|
50
53
|
readonly surface: Surface;
|
|
51
54
|
/**
|
|
52
55
|
* The app's root renderable, asked for once per frame, so a view of the
|
|
53
|
-
* app's state is drawn from the state as it is at that frame.
|
|
56
|
+
* app's state is drawn from the state as it is at that frame. `t` is the
|
|
57
|
+
* frame's time in seconds on the app's `clock`, which is what an effect is
|
|
58
|
+
* sampled at.
|
|
54
59
|
*/
|
|
55
|
-
readonly view: () => Renderable;
|
|
60
|
+
readonly view: (t: number) => Renderable;
|
|
61
|
+
/** What the app reads the time from and ticks on. The platform's, unless given. */
|
|
62
|
+
readonly clock?: Clock;
|
|
63
|
+
/** How often the app paints while something is animating. 30 frames a second, unless given. */
|
|
64
|
+
readonly rate?: FrameRate;
|
|
56
65
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
66
|
+
* The keys that stop the app, by `keyName`, heard before any `onKey`
|
|
67
|
+
* handler. `["ctrl+c"]` unless given; `[]` leaves every key to the program.
|
|
68
|
+
* A name no key goes by throws here.
|
|
59
69
|
*/
|
|
60
|
-
readonly
|
|
70
|
+
readonly quitKeys?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A turn of the wheel, where the pointer was when it turned: `lines` is
|
|
74
|
+
* positive towards the end of the content, a line a notch.
|
|
75
|
+
*/
|
|
76
|
+
export interface Wheel {
|
|
77
|
+
readonly x: number;
|
|
78
|
+
readonly y: number;
|
|
79
|
+
readonly lines: number;
|
|
61
80
|
}
|
|
81
|
+
declare const WHEEL_LINES: {
|
|
82
|
+
readonly scroll_up: -1;
|
|
83
|
+
readonly scroll_down: 1;
|
|
84
|
+
};
|
|
85
|
+
type WheelTurn = Extract<ScreenMouseEvent, {
|
|
86
|
+
type: keyof typeof WHEEL_LINES;
|
|
87
|
+
}>;
|
|
88
|
+
/** A pointer event `onMouse` hears: every one but the vertical wheel, which is `onWheel`'s. */
|
|
89
|
+
export type AppMouseEvent = Exclude<ScreenMouseEvent, WheelTurn>;
|
|
62
90
|
export declare class App {
|
|
63
91
|
private readonly host;
|
|
64
92
|
private readonly surface;
|
|
65
93
|
private readonly view;
|
|
94
|
+
private readonly clock;
|
|
95
|
+
private _rate;
|
|
96
|
+
private readonly animations;
|
|
97
|
+
private ticking;
|
|
66
98
|
private readonly console;
|
|
67
99
|
private _phase;
|
|
68
100
|
private _frame;
|
|
69
|
-
private
|
|
101
|
+
private readonly painter;
|
|
70
102
|
private refreshQueued;
|
|
71
103
|
private subscriptions;
|
|
72
104
|
private readonly paintHandlers;
|
|
105
|
+
private readonly keyHandlers;
|
|
106
|
+
private readonly mouseHandlers;
|
|
107
|
+
private readonly wheelHandlers;
|
|
108
|
+
private readonly quitKeys;
|
|
109
|
+
private readonly input;
|
|
73
110
|
private settle;
|
|
74
111
|
constructor(options: AppOptions);
|
|
75
112
|
get phase(): AppPhase;
|
|
113
|
+
/** How often the app paints while something is animating. */
|
|
114
|
+
get rate(): FrameRate;
|
|
115
|
+
/**
|
|
116
|
+
* Set how often the app paints: an animation under way goes on at the new
|
|
117
|
+
* rate from its next frame, without a frame lost or one painted twice.
|
|
118
|
+
*/
|
|
119
|
+
set rate(rate: FrameRate);
|
|
76
120
|
/** The rows on screen now, as painted: what a pointer event lands on. */
|
|
77
121
|
get frame(): readonly (readonly Segment[])[];
|
|
78
122
|
/**
|
|
@@ -93,6 +137,15 @@ export declare class App {
|
|
|
93
137
|
* for as long as it ran.
|
|
94
138
|
*/
|
|
95
139
|
refresh(): void;
|
|
140
|
+
/**
|
|
141
|
+
* Paint at the app's `rate` until the returned function is called: what an
|
|
142
|
+
* effect sampled at the frame's time asks for while it moves. The app ticks
|
|
143
|
+
* while anything is animating and it holds the terminal, and stops when
|
|
144
|
+
* nothing is, so a still app paints only when asked. The animation's first
|
|
145
|
+
* frame does not wait out an interval: at one frame every two seconds, that
|
|
146
|
+
* would hold its start off screen for two.
|
|
147
|
+
*/
|
|
148
|
+
animate(): Unsubscribe;
|
|
96
149
|
/**
|
|
97
150
|
* Hand the terminal to whatever launched the program, and take it back and
|
|
98
151
|
* repaint when that resumes it. What sends the app here is the app's own —
|
|
@@ -104,6 +157,26 @@ export declare class App {
|
|
|
104
157
|
* handler that throws fails the app as a frame that throws does.
|
|
105
158
|
*/
|
|
106
159
|
onPaint(handler: (frame: readonly (readonly Segment[])[]) => void): Unsubscribe;
|
|
160
|
+
/**
|
|
161
|
+
* Hear every key the app does not quit on, by the `KeyPress` the terminal
|
|
162
|
+
* reported; `keyName` names it. A handler that throws fails the app.
|
|
163
|
+
*/
|
|
164
|
+
onKey(handler: (press: KeyPress) => void): Unsubscribe;
|
|
165
|
+
/**
|
|
166
|
+
* Hear every pointer event but the vertical wheel, which is `onWheel`'s,
|
|
167
|
+
* in terminal cells. Only the alternate surface reports the pointer. A
|
|
168
|
+
* handler that throws fails the app.
|
|
169
|
+
*/
|
|
170
|
+
onMouse(handler: (event: AppMouseEvent) => void): Unsubscribe;
|
|
171
|
+
/**
|
|
172
|
+
* Offer every wheel turn to `handler`, which returns whether it moved
|
|
173
|
+
* something. A turn no handler moves anything with is passed outward to the
|
|
174
|
+
* host (`TerminalHost.passWheel`), so the page or terminal around the app
|
|
175
|
+
* scrolls instead. Handlers are offered a turn in the order they subscribed,
|
|
176
|
+
* and the first that moves something takes it. A handler that throws fails
|
|
177
|
+
* the app.
|
|
178
|
+
*/
|
|
179
|
+
onWheel(handler: (wheel: Wheel) => boolean): Unsubscribe;
|
|
107
180
|
/** Hand the terminal back for good; `run` resolves. */
|
|
108
181
|
stop(): void;
|
|
109
182
|
/**
|
|
@@ -116,6 +189,11 @@ export declare class App {
|
|
|
116
189
|
private enter;
|
|
117
190
|
private leave;
|
|
118
191
|
private end;
|
|
192
|
+
private key;
|
|
193
|
+
private mouse;
|
|
194
|
+
private hear;
|
|
195
|
+
private tick;
|
|
119
196
|
private paint;
|
|
120
197
|
private draw;
|
|
121
198
|
}
|
|
199
|
+
export {};
|